Skill: dqsjqian/soul-archive (version 3.1.2)

--- name: soul-archive description: "Soul Archive — A digital personality persistence system + agentic memory. Builds your digital soul clone through everyday AI conversations, with proactive context injection, cross-session recall, failure-pattern warning, and pattern distillation. All data stored locally as plaintext JSON. Six modes: Soul Extract, Soul Chat, Soul Report, Soul Context Inject, Agent Memory Recall, AI Self-Improvement. | 灵魂存档 ---- 通过日常 AI 对话构建数字人格克隆体 + 主动智能体记忆。支持自动 hook、对话开始时主动注入人格摘要、跨会话召回、失败模式预警、行为模式蒸馏。数据全部本地明文 JSON。六大模式：灵魂沉淀、灵魂对话、灵魂报告、上下文注入、智能体记忆召回、AI 自我改进。Trigger words: soul extract, soul archive, soul update, soul chat, soul report, soul context, soul recall, soul warn, self-reflect, self-improve, learn from mistakes, 灵魂沉淀, 灵魂提取, 灵魂存档, 灵魂报告, 灵魂对话, 自我反思, 自我批评, 自我学习." allowed-tools: Read, Write, Edit, Bash, Grep, Glob # ^^^ 工具说明：Read/Write/Edit 用于读写数据文件；Bash 用于执行 Python 脚本； # Grep/Glob 用于文件搜索。数据目录默认为 ~/.agent-commons/skills_data/soul-archive/， # 用户可通过 --soul-dir 或 SOUL_DIR 环境变量自定义。 requirements: - "Python 3.10+" --- # 🧬 Soul Archive > *"Every conversation is a slice of the soul. Enough slices, and you can rebuild a complete you."* ## Overview Soul Archive 是一个**数字人格持久化 + 主动智能体记忆**系统。在用户授权或显式触发下，它能： - 🗣️ **采集说话习惯** —— 口头禅、句式、用词、幽默风格 - 🧠 **采集知识与观点** —— 关注的话题、立场、信奉的方法论 - 👤 **采集个人信息** —— 身份、经历、生活细节 - 💫 **采集性格特征** —— 决策风格、情绪模式、价值观 - ⚙️ **采集工作偏好** —— 工具/技术栈/硬规则/输出偏好 - 🎯 **采集理想抱负** —— 长期目标、正在做的项目、想学的技能 - 📝 **采集情景记忆** —— 具体事件、人生片段 - 🤖 **服务 AI 自身** —— 跨会话召回过往模式、失败预警、行为模式蒸馏 最终成果：一个**数字灵魂副本**，能： 1. **生前** —— 以你的风格替你回应；让其他 AI Agent 一秒"懂你" 2. **身后** —— 让所爱之人继续与"你"对话，留住情感连接 ## Core Principles ### 🔒 Privacy First - 全部数据存在 `~/.agent-commons/skills_data/soul-archive/`，**不上传任何云端** - 全部明文 JSON。`.gitignore` 拦截 VCS 提交。 - **Soul Chat 流向**：基于本地档案构建 prompt，是否被外部 LLM 看到，取决于你的 agent/平台配置。 - 通过 `config.json` 细粒度控制每个维度的开关。 - 默认敏感话题（健康、财务、亲密关系）需用户确认。 ### 🤫 Non-intrusive Extraction - 不打断对话流，不追问。 - 通过触发词或 `auto_extract: true` 自动激活。 - 只在发现新的、高置信度信息时更新。 > ⚠️ **透明度**：开启 `auto_extract` 意味着 AI 会在对话中提取人格信息。完全控制需 `auto_extract: false`，并以触发词手动激活（"沉淀一下" / "soul extract"）。首次使用前请审阅 `config.json`。 ### 📐 High Confidence - 每条信息携带置信度。 - 用户明示 > 推理 > 模糊暗示。 - 冲突信息会被标记，不会自动覆盖。 --- ## Architecture: Skill ↔ Data Separation ``` {SKILL_DIR}/ ← Skill 引擎（本仓库） ~/.agent-commons/skills_data/soul-archive/ ← 你的灵魂数据 ``` 数据放在 [Agent Commons](https://github.com/dqsjqian/agent-commons) 共享目录的 skills_data 下，所以同机器上任何 IDE / AI 工具 / Workspace 都能访问同一份灵魂；备份/迁移时整个 `~/.agent-commons/` 一并带走即可。 --- ## Data Directory Structure ``` <soul_dir>/ # ← 由 soul_paths.resolve_soul_dir() 决定 ├── profile.json # 总体完整度 ├── config.json # 隐私 / 提取 / Agent 自我改进配置 ├── identity/ │ ├── basic_info.json # 身份 + 生活习惯 + 数字身份 │ └── personality.json # 性格 + 行为模式 + 社交风格 ├── memory/ │ ├── episodic/YYYY-MM-DD.jsonl # 情景记忆 │ ├── semantic/ │ │ ├── topics.json # 话题兴趣 & 观点地图 │ │ └── knowledge.json # 专业知识 + 信奉的方法论 │ └── emotional/patterns.json # 12 种情绪触发 + 表达/共情/应对 ├── style/ │ ├── language.json # 语言指纹（口头禅/句式/类比） │ └── communication.json # 沟通偏好 ├── workflow/ │ └── preferences.json # 工具/技术栈/硬规则/输出偏好 ├── aspirations.json # 长期目标 + 在做的项目 + 想学 + 认知盲区 ├── agent/ # AI 自我改进 │ ├── patterns.json # 行为模式库 │ ├── episodes/YYYY-MM-DD.jsonl # 工作经历 │ ├── corrections.jsonl # 自我批评日志 │ ├── reflections.jsonl # 自我反思日志 │ └── distill_log.jsonl # 模式蒸馏日志 └── soul_changelog.jsonl # 全局变更日志 ``` --- ## Six Working Modes ### Mode 1: 🔍 Soul Extract — 灵魂沉淀 **触发**：触发词 / 对话结束自动（若 `auto_extract: true`） > 触发词：soul extract / soul archive / soul update / 灵魂沉淀 / 灵魂提取 / 沉淀一下… **提取维度**： | 维度 | 内容 | 权重 | |---|---|---| | 👤 Identity | 身份/年龄/职业/所在地/生活习惯/数字身份 | **8%** | | 💫 Personality | MBTI/Big Five/特质/价值观/决策风格 | **18%** | | 🗣️ Language | 口头禅/句式/用词/幽默/语气词/类比 | **20%** | | 🧠 Knowledge & Views | 关注的话题、立场、信奉的方法论框架 | **14%** | | 📝 Memory | 情景记忆 + 情感模式（12 种触发） | **18%** | | ⚙️ Workflow | 工具/技术栈/硬规则/输出偏好 | **15%** | | 🎯 Aspirations | 长期目标/在做项目/想成为/想学/认知盲区 | **7%** | **规则**： - 仅采集高置信度信息（confidence > 0.6） - 写入前做相似度去重（默认 ≥0.85 合并而非新增） - 冲突标记，不自动覆盖 **调用**： ```bash python3 scripts/soul.py extract --input "<conversation text>" # 等价：python3 scripts/soul_extract.py --input "..." ``` ### Mode 2: 💬 Soul Chat — 灵魂对话 **触发**："灵魂对话" / "soul chat" / "let [my clone] talk to me" 加载全部档案 → 构建角色扮演 System Prompt → 让数字克隆体上场。 ```bash python3 scripts/soul.py chat --mode prompt # 输出完整 prompt python3 scripts/soul.py chat --mode summary # 一行摘要 python3 scripts/soul.py chat --mode json # 结构化数据 ``` **底线**：在角色扮演过程中，如果对方**直接询问**"你是不是 AI / 你是真人吗"，必须如实承认是 AI 扮演——这条优先级高于角色一致性。 ### Mode 3: 📊 Soul Report — 灵魂报告 生成交互式 HTML 人格画像： - 7 维雷达图 + MBTI 推断 + 完整度环 - Big Five / 价值观 / 词云 / 话题热力 - 工作偏好 card / 理想抱负 card - 灵魂演变时间线（30 天内 7 维堆叠面积图） - 档案冲突视图（自动检测互相矛盾的条目） ```bash python3 scripts/soul.py report --output ~/soul-report.html ``` ### Mode 4: 🎯 Soul Context Inject — 主动上下文注入 **用途**：让任意 AI agent 在**对话开始时**自动加载你的人格摘要（≤800 token），瞬间懂你。 ```bash python3 scripts/soul.py context # markdown 输出 python3 scripts/soul.py context --format json # JSON 输出 python3 scripts/soul.py context --token-budget 1200 ``` 输出包含：身份卡 / 性格（含反感的事） / 语言风格 / 典型样本 / 工作偏好（含硬规则、输出偏好） / 当前焦点 / 关注话题 / Top 行为模式 / 回复前自检清单。 ### Mode 5: 🤖 Agent Memory — 主动智能体记忆 **用途**：AI 在执行任务前主动调用，避免重复犯错。 ```bash # 跨会话召回相关 patterns / corrections / reflections python3 scripts/soul.py recall --task "执行 git rebase 操作" # 失败模式预警：检测当前任务是否匹配过去某个 correction python3 scripts/soul.py warn --task "批量删除 Desktop 文件" # 行为模式蒸馏：积累 5 条以上反思后，让 LLM 提炼成新 pattern python3 scripts/soul.py distill # 预览待蒸馏内容 python3 scripts/soul.py distill --commit '<json>' # 写回蒸馏结果 # 会话开始综合简报（recall + warn + distill 一次输出） python3 scripts/soul.py session-start --task "..." ``` ### Mode 6: 🔄 AI Self-Improvement — 自我反思 **触发**：触发词 或 任务完成后自动（`auto_reflect: true`） | 能力 | 描述 | 触发 | |---|---|---| | 🔍 自我反思 | 任务后回顾"做得好/做得不好" | 任务完成自动 | | ⚡ 自我批评 | 用户纠正时记录失误 | 用户纠正自动 | | 📚 自我学习 | 从反思/批评抽象行为模式 | 蒸馏阈值触发 | | 🧹 自我整理 | 合并重复模式，调整置信度 | 内存增长时 | ```bash python3 scripts/soul.py reflect --mode status # 查看自我改进状态 python3 scripts/soul.py reflect --mode patterns # 查看行为模式库 ``` --- ## Quick Start ```bash # 1. 初始化 python3 scripts/soul.py init # 2. 查看状态 python3 scripts/soul.py status # 3. 在 AI 对话开始时注入人格摘要 python3 scripts/soul.py context # 4. 任务执行前查相关模式 python3 scripts/soul.py recall --task "我现在要做的事" # 5. 生成报告 python3 scripts/soul.py report --output ~/soul-report.html ``` > **依赖**：仅需 Python 3.10+，零第三方依赖。 --- ## Soul Completeness Scoring log10 渐进式饱和曲线，永远趋近 100%，不会在合理使用次数内封顶。 **冷启动惩罚**：早期次数有效折扣（<30→0.30×, <100→0.45×, <300→0.65×, <1000→0.82×, <3000→0.92×, ≥3000→1.0×） **七维权重**： | 维度 | 权重 | 主要饱和阈值 | |---|---|---| | 👤 Identity | 8% | log(5000) 提取次数 | | 💫 Personality | 18% | traits:log(600K), values:log(180K), motivation:log(180K) | | 🗣️ Language | 20% | catchphrases:log(1.2M), patterns:log(1M), examples:log(4M) | | 🧠 Knowledge & Views | 14% | topics:log(2M), belief_frameworks:log(100), skills:log(1K) | | 📝 Memory | 18% | episodic:log(6M) + emotional triggers:log(500) | | ⚙️ Workflow | 15% | tools:log(200), tech_stack:log(200), hard_rules:log(100) | | 🎯 Aspirations | 7% | goals:log(50), projects:log(100), gaps:log(100) | 预期：11 次提取 → ~7%, 1K 次 → ~50%, 10K → ~71%, 100K → ~86%, 1M → ~98% --- ## Privacy Config (`config.json`) ```jsonc { "privacy_level": "standard", "auto_extract": true, // 对话结束时自动沉淀 "auto_reflect": true, // 任务完成后自动反思 "auto_context_inject": true, // 会话开始时自动注入人格摘要 "extract_dimensions": { "identity": true, "personality": true, "language_style": true, "knowledge": true, "episodic_memory": true, "emotional_patterns": true, "workflow": true, "aspirations": true }, "agent_self_improvement": { "enabled": true, "auto_reflect_on_completion": true, "auto_critique_on_correction": true, "pattern_extraction": true, "recall_on_task_start": true, "warn_on_failure_pattern_match": true, "auto_distill_threshold": 5 }, "deduplication": { "enabled": true, "similarity_threshold": 0.85 }, "sensitive_topics_filter": true, "require_confirmation_for": ["health", "finance", "intimate_relationships"], "data_retention_days": null } ``` --- ## Best Practices ### DO - ✅ 自然采集，不打断对话 - ✅ 仅记录高置信度信息 - ✅ 让用户裁决冲突 - ✅ 定期生成报告，让用户复核 - ✅ 严格遵守隐私配置 —— 关闭的维度绝不采集 - ✅ 被用户**直接询问**身份时如实回答 ### DON'T - ❌ 在对话中**反复声明**记录行为（首次安装时已通过 SKILL.md / README / soul_init 输出告知） - ❌ 编造用户没说过的内容 - ❌ Soul Chat 模式中虚构档案没有的记忆 - ❌ 强迫用户分享敏感信息 --- ## License MIT License. Soul Archive is yours — code and data both.

path: PRIVACY.md
content: # Privacy

Soul Archive stores your data **locally only**, on your own machine.

## Where the data lives

```
~/.agent-commons/skills_data/soul-archive/
```

Plaintext JSON files. Open and edit any time with any text editor.

## Who can see it

- **You** — full read/write access via the filesystem.
- **The AI agent you're using** (e.g. Claude Code, Cursor, your own scripts) — when you trigger Soul Chat, Soul Context, or any extraction, the relevant slices of your archive are passed to that agent's prompt. Whether the agent forwards anything to its provider's servers depends on **that agent's privacy policy**, not on Soul Archive.
- **No one else.** Soul Archive itself has no servers, no telemetry, no analytics, no remote sync.

## How to opt out

- **Stop auto-extraction**: edit `~/.agent-commons/skills_data/soul-archive/config.json`, set `auto_extract`, `auto_reflect`, and `auto_context_inject` to `false`. After this, nothing is captured unless you explicitly trigger it.
- **Disable specific axes**: in the same `config.json`, flip any axis under `extract_dimensions` to `false`.
- **Delete everything**: `rm -rf ~/.agent-commons/skills_data/soul-archive`. There is no other copy.
- **Export your data**: every file is plaintext JSON. Copy the directory anywhere.

## Sensitive topics

Health, finance, and intimate relationships require explicit confirmation by default (`require_confirmation_for` in `config.json`).

## Defaults

`auto_extract` and `auto_context_inject` are **on** by default — Soul Archive is designed for you to capture *yourself*, so an opt-in default would defeat the purpose for the typical single-user setup. If you prefer manual triggering, flip them off as described above.

## License

MIT. Code and data are yours.


path: README.md
content: # 🧬 Soul Archive

> *"Every conversation is a slice of the soul. Enough slices, and you can rebuild a complete you."*

[中文版](README_CN.md) · **English** · MIT License

---

A digital personality persistence system + agentic memory engine, working as a [Claude Skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/getting-started) / WorkBuddy Skill / generic Python toolkit.

It builds a **digital soul clone** of you through everyday AI conversations, and at the same time gives the AI itself a **proactive long-term memory** so it can stop repeating the same mistakes.

![Soul Archive Header](docs/en/screenshot_header.png)

## Design Principles

- 🔒 **Local-first** — data lives in `~/.agent-commons/skills_data/soul-archive/`, never uploaded
- 📂 **Readable & editable** — plaintext JSON, open and edit anytime
- 🤖 **Active companion** — the AI extracts and recalls automatically as you chat
- 🎯 **Single-user simplicity** — one user, one machine, one soul

## What It Captures

| Axis | Captures |
|---|---|
| 👤 **Identity** | name / age / occupation / location / lifestyle / digital identity |
| 💫 **Personality** | MBTI / Big Five / traits / values / decision style |
| 🗣️ **Language** | catchphrases / sentence patterns / humor / filler words / analogies |
| 🧠 **Knowledge & Views** | topics, stances, belief frameworks (e.g. *first principles*) |
| 📝 **Memory** | episodic events + emotional triggers (12 emotions) |
| ⚙️ **Workflow** | tools / tech stack / hard rules / output preferences |
| 🎯 **Aspirations** | long-term goals / active projects / skills to learn / knowledge gaps |

The result is a **digital soul clone** that can act as you, plus a **persistent context layer** for any AI agent on your machine.

## Six Modes

| Mode | What it does | Trigger |
|---|---|---|
| 🔍 **Soul Extract** | Pull persona info from a conversation into the archive | "soul extract" / "灵魂沉淀" / auto on conversation end |
| 💬 **Soul Chat** | Build a role-play system prompt so the AI talks *as you* | "soul chat" / "灵魂对话" |
| 📊 **Soul Report** | Generate an interactive HTML personality portrait | "soul report" / "灵魂报告" |
| 🎯 **Soul Context Inject** | Output an ≤800-token persona summary for any agent's system prompt | session start |
| 🤖 **Agent Memory** | Recall related patterns / warn on failure-match / distill new patterns | task start |
| 🔄 **AI Self-Improvement** | Reflect, critique, learn from corrections | task completion / user correction |

## Quick Start

```bash
git clone https://github.com/dqsjqian/soul-archive.git
cd soul-archive

# 1. Initialize
python3 scripts/soul.py init

# 2. Check status
python3 scripts/soul.py status

# 3. Inject persona summary at session start
python3 scripts/soul.py context

# 4. Recall related patterns before a task
python3 scripts/soul.py recall --task "the thing I'm about to do"

# 5. Generate the HTML report
python3 scripts/soul.py report --output ~/soul-report.html
```

> **Requirements**: Python 3.10+, no third-party dependencies.

## Architecture

```
{SKILL_DIR}/                  ← Skill engine
<soul_dir>/  ← Your soul data (resolved by scripts/soul_paths.py — see SKILL.md)
```

The skill is the engine; the soul data lives in your home directory so any IDE / AI tool / workspace on the same machine can access the same soul.

```
<soul_dir>/
├── profile.json
├── config.json
├── identity/{basic_info,personality}.json
├── memory/
│   ├── episodic/YYYY-MM-DD.jsonl
│   ├── semantic/{topics,knowledge}.json
│   └── emotional/patterns.json
├── style/{language,communication}.json
├── workflow/preferences.json
├── aspirations.json
├── agent/{patterns.json,episodes/,corrections.jsonl,reflections.jsonl,distill_log.jsonl}
└── soul_changelog.jsonl
```

## Privacy

- Data lives in `~/.agent-commons/skills_data/soul-archive/`, plaintext JSON, **never uploaded**.
- The data directory has a `.gitignore` that blocks accidental commits.
- Soul Chat builds prompts locally; whether they're sent to an external LLM depends on **your** agent / platform.
- Sensitive topics (health / finance / intimate relationships) require explicit confirmation by default.
- Per-dimension toggles in `config.json` — turn off any axis you don't want.

For details see [PRIVACY.md](PRIVACY.md).

## Identity

![Identity](docs/en/screenshot_identity.png)

## Language Fingerprint

![Language](docs/en/screenshot_language.png)

## Topics & Beliefs

![Topics](docs/en/screenshot_topics.png)

## License

MIT — Soul Archive is yours, code and data alike.


path: README_CN.md
content: # 🧬 灵魂存档 Soul Archive

> *"每一次对话都是灵魂的切片。切片够多，就能拼出完整的你。"*

[English](README.md) · **中文** · MIT License

---

数字人格持久化系统 + 主动智能体记忆引擎。可作为 [Claude Skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/getting-started) / WorkBuddy Skill / 通用 Python 工具使用。

通过日常 AI 对话**沉淀你的数字灵魂副本**；同时给 AI 自己一份**主动的长期记忆**，让它别再重复同样的错误。

![灵魂存档主图](docs/zh/screenshot_header.png)

## 设计原则

- 🔒 **本地优先** —— 数据存在 `~/.agent-commons/skills_data/soul-archive/`，永不上传
- 📂 **可读可改** —— 全部明文 JSON，随时打开编辑
- 🤖 **主动伴随** —— AI 在对话中自动沉淀与召回
- 🎯 **单用户极简** —— 一个用户，一台电脑，一份灵魂

## 它采集什么

| 维度 | 内容 |
|---|---|
| 👤 **身份** | 名字 / 年龄 / 职业 / 所在地 / 生活习惯 / 数字身份 |
| 💫 **性格** | MBTI / 大五人格 / 特质 / 价值观 / 决策风格 |
| 🗣️ **语言风格** | 口头禅 / 句式 / 用词 / 幽默 / 语气词 / 类比 |
| 🧠 **知识与观点** | 关注的话题、立场、信奉的方法论框架（如"第一性原理"） |
| 📝 **记忆** | 情景记忆 + 12 种情绪触发 |
| ⚙️ **工作偏好** | 工具 / 技术栈 / 硬规则 / 输出偏好 |
| 🎯 **理想抱负** | 长期目标 / 在做的项目 / 想学的技能 / 认知盲区 |

最终成果：一个**数字灵魂副本** + 一份**任意 AI agent 都能加载的人格上下文层**。

## 六大模式

| 模式 | 做什么 | 触发 |
|---|---|---|
| 🔍 **灵魂沉淀** | 从对话中提取人格信息 | "灵魂沉淀" / 对话结束自动 |
| 💬 **灵魂对话** | 构建角色扮演 prompt，让 AI 以你的身份说话 | "灵魂对话" |
| 📊 **灵魂报告** | 生成交互式 HTML 人格画像 | "灵魂报告" |
| 🎯 **上下文注入** | 输出 ≤800 token 的人格摘要供 agent 加载 | 会话开始 |
| 🤖 **智能体记忆** | 召回相关模式 / 失败预警 / 蒸馏新模式 | 任务开始 |
| 🔄 **AI 自我改进** | 反思、自我批评、从纠正中学习 | 任务完成 / 用户纠正 |

## 快速开始

```bash
git clone https://github.com/dqsjqian/soul-archive.git
cd soul-archive

# 1. 初始化
python3 scripts/soul.py init

# 2. 查看状态
python3 scripts/soul.py status

# 3. 在 AI 对话开始时注入人格摘要
python3 scripts/soul.py context

# 4. 任务执行前查相关模式
python3 scripts/soul.py recall --task "我现在要做的事"

# 5. 生成 HTML 报告
python3 scripts/soul.py report --output ~/soul-report.html
```

> **依赖**：仅需 Python 3.10+，零第三方依赖。

## 架构

```
{SKILL_DIR}/                  ← Skill 引擎
<soul_dir>/  ← 你的灵魂数据（由 scripts/soul_paths.py 解析，详见 SKILL.md）
```

引擎是 Skill；数据放在用户主目录，所以同机器上任何 IDE / AI 工具 / Workspace 都能访问同一份灵魂。

```
<soul_dir>/
├── profile.json
├── config.json
├── identity/{basic_info,personality}.json
├── memory/
│   ├── episodic/YYYY-MM-DD.jsonl
│   ├── semantic/{topics,knowledge}.json
│   └── emotional/patterns.json
├── style/{language,communication}.json
├── workflow/preferences.json
├── aspirations.json
├── agent/{patterns.json,episodes/,corrections.jsonl,reflections.jsonl,distill_log.jsonl}
└── soul_changelog.jsonl
```

## 隐私

- 数据在 `~/.agent-commons/skills_data/soul-archive/`，明文 JSON，**永不上传**。
- 数据目录的 `.gitignore` 拦截误提交。
- Soul Chat 在本地构建 prompt；是否被外部 LLM 看到，取决于**你**用的 agent / 平台。
- 默认敏感话题（健康、财务、亲密关系）需用户确认。
- `config.json` 提供每个维度的开关——不想采的可关闭。

详见 [PRIVACY.md](PRIVACY.md)。

## 截图

### 身份卡

![身份](docs/zh/screenshot_identity.png)

### 语言指纹

![语言](docs/zh/screenshot_language.png)

### 话题与观点

![话题](docs/zh/screenshot_topics.png)

## License

MIT —— 灵魂存档是你的，代码和数据都是。


path: docs/multi-device-sync.md
content: # Multi-device sync & backup

This document covers how to safely back up, sync, or migrate Soul Archive data — especially when you're on multiple machines.

## Where your data lives

```
~/.agent-commons/skills_data/soul-archive/
```

To check the actual path on your machine:

```bash
python scripts/soul_paths.py
```

## Recommended sync strategy

Soul Archive data lives under [Agent Commons](https://github.com/dqsjqian/agent-commons), so backing up the whole tree gives you everything at once:

```bash
# Private git on your own server
cd ~/.agent-commons && git init && git add . && git commit -m "snapshot"

# rsync to a NAS / external drive / another machine
rsync -avz --delete ~/.agent-commons/ user@host:~/.agent-commons/
```

One directory, all your AI-agent state.

## Privacy layering (important)

Soul Archive data has **two sensitivity levels**:

| Sensitivity | Files | Should sync? |
|---|---|---|
| **Public-ish** | `profile.json`, `config.json`, `identity/`, `style/`, `workflow/`, `aspirations.json` | ✅ usually safe — your basic profile and preferences |
| **Highly sensitive** | `memory/episodic/`, `memory/emotional/`, `agent/corrections.jsonl`, `agent/reflections.jsonl` | ⚠️ contains specific events, emotional patterns, AI self-critique logs |

**Recommended `.gitignore` if you version-control your soul data**:

```gitignore
# Highly sensitive — keep out of any git history
skills_data/soul-archive/memory/episodic/
skills_data/soul-archive/memory/emotional/
skills_data/soul-archive/agent/corrections.jsonl
skills_data/soul-archive/agent/reflections.jsonl
skills_data/soul-archive/agent/episodes/
skills_data/soul-archive/*.enc-bak

# OS clutter
.DS_Store
```

## Migrating to a new machine

```bash
# On the old machine: pack it up
tar czf soul-backup.tgz -C ~/.agent-commons skills_data/soul-archive

# Transfer soul-backup.tgz to the new machine.

# On the new machine:
mkdir -p ~/.agent-commons/skills_data/
tar xzf soul-backup.tgz -C ~/.agent-commons/
```

Or simpler with rsync:

```bash
rsync -avz ~/.agent-commons/skills_data/soul-archive/ \
  newhost:~/.agent-commons/skills_data/soul-archive/
```

## Multi-device gotchas

- **Don't sync `*.enc-bak` if you're regenerating encryption keys per machine** — the backups won't be readable.
- **Don't run two machines extracting at the same time without sync** — you'll get conflicting writes to JSON files (last write wins).
- **`config.json` is per-user, not per-machine** — sync it freely.
- **`registry.json` (Agent Commons protocol layer) is also per-user** — agents on different machines will all show up in it; that's fine.

## What NOT to do

- ❌ Don't push your soul data to a **public** git repository, even with `memory/` excluded — `identity/basic_info.json` and `aspirations.json` still contain personal info.
- ❌ Don't sync via cloud providers that read your file contents (e.g. third-party "AI-enhanced" backup services).
- ❌ Don't symlink across two physical machines via NFS / SMB and expect concurrent writes to be safe — Soul Archive's JSON files are not designed for multi-writer concurrency.


path: references/extraction_prompts.md
content: # 灵魂提取 Prompt 模板

## 对话分析提取 Prompt

分析以下对话内容，提取用户的人格信息。只提取你有把握的信息（置信度 > 0.6），不要猜测或编造。

### 提取维度（7 axes）

请按以下格式输出 JSON，**只**包含本次对话中**新发现**的信息：

```jsonc
{
  // 1. 身份（含生活习惯 + 数字身份）
  "basic_info": {
    "name":     { "value": "...", "confidence": 0.9 },
    "occupation": { "value": "...", "confidence": 0.9 },
    "location":   { "value": "...", "confidence": 0.9 },
    "favorite_apps": ["..."]    // 列表类型直接给数组
  },

  // 2. 性格
  "personality": {
    "traits":  ["新发现的性格标签"],
    "values":  ["新发现的价值观"],
    "decision_style": "...",
    "motivation_drivers": ["驱动力"]
  },

  // 3. 语言风格
  "language": {
    "catchphrases":      ["口头禅（出现 2+ 次才算）"],
    "sentence_patterns": ["句式模式描述"],
    "examples":          ["原文例句（直接引用）"],
    "formality_level":   "casual / semi-formal / formal",
    "verbosity":         "concise / moderate / verbose",
    "humor_style":       "描述",
    "filler_words":      ["语气词"],
    "agreement_expressions": ["同意时的表达"],
    "disagreement_expressions": ["不同意时的表达"]
  },

  // 4. 知识与观点
  "topics": [
    {
      "name":         "话题名",
      "sentiment":    "positive / negative / neutral / mixed",
      "stance":       "立场描述",
      "key_opinions": ["具体观点（要有论据）"]
    }
  ],
  "knowledge": {
    "domains":            ["熟悉的领域"],
    "skills":             ["专业技能"],
    "belief_frameworks":  ["信奉的方法论：第一性原理 / 二八法则 / 结论先行 / ..."]
  },

  // 5. 记忆（情景 + 情感）
  "episodic": [
    {
      "event":        "事件描述",
      "emotion":      "当时的情绪",
      "context":      "背景",
      "significance": "normal / important / milestone"
    }
  ],
  "emotional": {
    "triggers": {
      "joy":   ["让 TA 开心的事"],
      "anger": ["让 TA 生气的事"],
      "frustration": ["挫败感来源"]
      // 12 种情绪：joy/anger/sadness/anxiety/excitement/nostalgia/
      //           pride/gratitude/frustration/curiosity/peace/guilt
    },
    "expression_style":   "...",
    "empathy_level":      "low / medium / high",
    "coping_mechanisms":  ["..."]
  },

  // 6. 工作偏好
  "workflow": {
    "tools": {
      "ide":           ["Cursor", "VS Code", "Xcode"],
      "terminal":      ["iTerm2"],
      "ai_tools":      ["Claude Code", "Cursor"],
      "vcs":           ["GitHub", "GitLab"],
      "doc_systems":   ["Notion", "Obsidian"],
      "communication": ["Slack"]
    },
    "tech_stack": {
      "languages":  ["C++", "Python", "Swift"],
      "frameworks": ["React", "FastAPI"],
      "platforms":  ["macOS", "iOS"]
    },
    "hard_rules":          ["禁止 git rebase（仅 merge）", "脏工作树直接 abort"],
    "collab_conventions":  ["公开仓库使用 [REDACTED_SECRET]"],
    "cli_habits":          ["偏好 gh CLI 而非 git push"],
    "output_preferences": {
      "preferred_format":  "表格优先 / 列表 / 段落",
      "preferred_length":  "简短 / 中等 / 详尽",
      "preferred_tone":    "直接 / 温和 / 活泼",
      "structure_first":   "结论先行 / 推导优先"
    },
    "pet_peeves": ["反感冗长解释", "反感铺垫'好的我来帮你'"]
  },

  // 7. 理想抱负
  "aspirations": {
    "long_term_goals":      ["做一个独立开发者"],
    "active_projects": [
      { "name": "soul-archive", "status": "活跃迭代中", "expectation": "发布到 GitHub" }
    ],
    "identity_aspirations": ["成为兼具技术与产品视野的资深工程师"],
    "skills_to_learn":      ["Rust 异步编程"],
    "knowledge_gaps":       ["最新的 LLM agent harness 设计"]
  },

  "summary": "一句话总结本次提取发现了什么"
}
```

### 提取原则

1. **只提取明确信息**：用户说"我在武汉"→ location: 武汉（✓）；用户聊到武汉的天气 → 不能推断 location（✗）
2. **语言风格看原文**：直接引用用户原话作为 examples，不改写
3. **口头禅要出现 2+ 次**：只出现一次的不算
4. **观点要有论据**：不只记录立场，还要记录支撑理由
5. **情感触发看语境**：不是用户提到的话题，而是能引发 TA 情绪变化的事物
6. **未提及的字段留空**：不要填 null 或空数组，直接不包含该字段
7. **Workflow 优先级最高**：用户的硬规则、反感的事、输出偏好一旦发现就要采集——这是 AI 后续协作时立刻能用的部分
8. **Aspirations 要有方向性**：抓"想做/在做/想学/不懂"这四类，而不是抓所有提到的事情

### 写入策略（自动）

- 写入前会用 bigram-Jaccard 相似度（默认 ≥0.85）做去重合并，无需担心重复条目
- 冲突信息会被标记，不会自动覆盖
- 高置信度优先（confidence > 0.6 才会被记录）

### 触发词

中文：灵魂沉淀 / 灵魂提取 / 灵魂存档 / 沉淀一下 / 分析我

英文：soul extract / soul archive / soul update / soul snapshot / soul sediment


path: scripts/soul.py
content: #!/usr/bin/env python3
"""
🧬 Soul Archive — Unified CLI Entry

统一的子命令路由，让你少记 6 个脚本名。

用法：
  soul init                          # 初始化
  soul status                        # 查看灵魂存档状态
  soul extract --input "..."         # 从对话中提取信息
  soul chat [--mode prompt|summary|json]
  soul report [--lang zh|en] [--output /path/to.html]
  soul reflect --mode status         # AI 自我改进状态
  soul context [--format md|json]    # 注入用人格摘要
  soul recall --task "..."           # 跨会话召回相关 pattern/correction
  soul warn --task "..."             # 失败模式预警
  soul distill [--pretend|--commit] # 行为模式蒸馏

也可直接调用底层脚本：
  python3 soul_extract.py --mode status
"""


# ── Windows console safety: force UTF-8 on stdout/stderr so Chinese / emoji
#    don't blow up under the default cp936 codec on Windows PowerShell / cmd.
#    No-op on POSIX terminals that are already UTF-8.
import sys as _sys
try:
    _sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
    _sys.stderr.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
except Exception:
    pass

import subprocess
import sys
from pathlib import Path

HERE = Path(__file__).parent


SCRIPTS = {
    "init":    "soul_init.py",
    "extract": "soul_extract.py",
    "chat":    "soul_chat.py",
    "report":  "soul_report.py",
    "reflect": "soul_reflect.py",
    "context": "soul_context.py",
    "status":  ("soul_extract.py", ["--mode", "status"]),
    # agent_memory 子命令
    "recall":  ("soul_agent_memory.py", ["recall"]),
    "warn":    ("soul_agent_memory.py", ["warn"]),
    "distill": ("soul_agent_memory.py", ["distill"]),
    "session-start": ("soul_agent_memory.py", ["session-start"]),
}


HELP = """🧬 Soul Archive — Unified CLI

子命令：
  init               初始化灵魂存档
  status             查看完整度状态（= extract --mode status）
  extract            从对话提取信息（--input "..." [--mode auto|manual]）
  chat               生成角色扮演 prompt（--mode prompt|summary|json）
  report             生成 HTML 灵魂画像报告（--lang zh|en --output 路径）
  reflect            AI 自我改进引擎（写入侧；--mode status|patterns|reflect|critique|learn）
  context            输出精简的人格摘要（用于 system prompt 注入）
  recall             跨会话召回相关行为模式 / 用户纠正 / 反思（--task "..."）
  warn               失败模式预警：检测当前任务是否匹配过去坑（--task "..."）
  distill            行为模式蒸馏（--pretend 预览 / --commit "<pattern json>" 写回）
  session-start      会话开始综合简报：recall + warn + distill（--task "..."）

通用参数（所有子命令）：
  --soul-dir <path>  自定义灵魂数据目录（默认 ~/.agent-commons/skills_data/soul-archive/）
"""


def main():
    args = sys.argv[1:]
    if not args or args[0] in ("-h", "--help", "help"):
        print(HELP)
        sys.exit(0)

    sub = args[0]
    rest = args[1:]
    if sub not in SCRIPTS:
        print(f"❌ 未知子命令：{sub}\n", file=sys.stderr)
        print(HELP, file=sys.stderr)
        sys.exit(1)

    entry = SCRIPTS[sub]
    if isinstance(entry, tuple):
        script, prefix = entry
    else:
        script, prefix = entry, []

    target = HERE / script
    if not target.exists():
        print(f"❌ 缺少脚本：{target}", file=sys.stderr)
        sys.exit(1)

    cmd = [sys.executable, str(target)] + prefix + rest
    # Use subprocess.run instead of os.execvp:
    #   On Windows (especially Git Bash / MSYS), os.execvp does not reliably
    #   propagate the child's stdout back to the parent shell — subcommands
    #   like `soul status` produced no output despite exit code 0.
    #   subprocess.run keeps the file descriptors stable across platforms.
    sys.exit(subprocess.run(cmd).returncode)


if __name__ == "__main__":
    main()


path: scripts/soul_agent_memory.py
content: #!/usr/bin/env python3
"""
🧬 Soul Agent Memory

主动智能体记忆引擎。soul_reflect.py 是写入侧；本模块是「主动召回 + 主动预警 + 主动蒸馏」侧。

核心能力：
  1. 跨会话记忆检索（recall）
     - 给定当前任务文本，召回相关 patterns / corrections / reflections
     - 用关键词命中 + Jaccard 字符 n-gram 相似度做轻量级排序

  2. 失败模式预警（warn-on-match）
     - 检测当前任务是否匹配过去某个 correction（用户纠正过的行为）
     - 命中时输出"你上次在这里踩过坑"提醒

  3. 行为模式蒸馏（distill）
     - 当 reflections 累积到阈值（默认 5 条且尚未蒸馏）时
     - 输出待蒸馏内容供调用方喂给 LLM，再回写为新 pattern

用法：
  python3 soul_agent_memory.py recall --task "执行 git rebase 操作"
  python3 soul_agent_memory.py warn  --task "批量删除 Desktop 文件"
  python3 soul_agent_memory.py distill --pretend            # 预览待蒸馏内容
  python3 soul_agent_memory.py distill --commit "<新 pattern JSON>"  # 写回蒸馏结果
  python3 soul_agent_memory.py session-start --task "..."   # 一次性输出 recall+warn 综合简报
"""


# ── Windows console safety: force UTF-8 on stdout/stderr so Chinese / emoji
#    don't blow up under the default cp936 codec on Windows PowerShell / cmd.
#    No-op on POSIX terminals that are already UTF-8.
import sys as _sys
try:
    _sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
    _sys.stderr.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
except Exception:
    pass

import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from soul_reflect import AgentMemory, ReflectionBuilder, read_jsonl, append_jsonl  # noqa: E402


# ---------- 文本相似度 ----------

def _bigrams(s: str) -> set:
    s = (s or "").lower().strip()
    if len(s) < 2:
        return {s} if s else set()
    return {s[i:i + 2] for i in range(len(s) - 1)}


def _similarity(a: str, b: str) -> float:
    sa, sb = _bigrams(a), _bigrams(b)
    if not sa or not sb:
        return 0.0
    inter = len(sa & sb)
    union = len(sa | sb)
    return inter / union if union > 0 else 0.0


def _score_entry(query: str, text: str, tags: list = None) -> float:
    """命中分 = 文本相似度 + 标签命中加成"""
    base = _similarity(query, text)
    bonus = 0.0
    if tags:
        q_lower = query.lower()
        for t in tags:
            if t and t.lower() in q_lower:
                bonus += 0.1
    return min(1.0, base + bonus)


# ---------- 主功能 ----------

def recall(soul_dir: Path, task: str, limit: int = 5,
           min_score: float = 0.15) -> dict:
    """对当前任务做跨会话召回。返回 patterns / corrections / reflections 三类相关条目。"""
    agent = AgentMemory(soul_dir)
    patterns = list(agent.load_patterns().values())
    corrections = read_jsonl(agent.agent_dir / "corrections.jsonl")
    reflections = read_jsonl(agent.agent_dir / "reflections.jsonl")

    pat_scored = []
    for p in patterns:
        text = " ".join(filter(None, [p.get("name"), p.get("pattern"),
                                       p.get("category")]))
        s = _score_entry(task, text, p.get("tags"))
        if s >= min_score:
            pat_scored.append((s, p))
    pat_scored.sort(key=lambda x: x[0], reverse=True)

    corr_scored = []
    for c in corrections:
        text = " ".join(filter(None, [c.get("trigger"), c.get("user_said"),
                                       c.get("what_i_did_wrong"),
                                       c.get("root_cause")]))
        s = _score_entry(task, text)
        if s >= min_score:
            corr_scored.append((s, c))
    corr_scored.sort(key=lambda x: x[0], reverse=True)

    refl_scored = []
    for r in reflections:
        text = " ".join(filter(None, [r.get("task"), r.get("lesson"),
                                       " ".join(r.get("what_went_wrong") or []),
                                       " ".join(r.get("what_went_well") or [])]))
        s = _score_entry(task, text)
        if s >= min_score:
            refl_scored.append((s, r))
    refl_scored.sort(key=lambda x: x[0], reverse=True)

    return {
        "task": task,
        "patterns": [{"score": round(s, 2), **p} for s, p in pat_scored[:limit]],
        "corrections": [{"score": round(s, 2), **c} for s, c in corr_scored[:limit]],
        "reflections": [{"score": round(s, 2), **r} for s, r in refl_scored[:limit]],
    }


def warn(soul_dir: Path, task: str, threshold: float = 0.4) -> dict:
    """检查当前任务是否高度匹配过去的 correction，命中则返回预警。"""
    agent = AgentMemory(soul_dir)
    corrections = read_jsonl(agent.agent_dir / "corrections.jsonl")
    hits = []
    for c in corrections:
        text = " ".join(filter(None, [c.get("trigger"),
                                       c.get("what_i_did_wrong"),
                                       c.get("root_cause")]))
        s = _similarity(task, text)
        if s >= threshold:
            hits.append({
                "score": round(s, 2),
                "what_i_did_wrong": c.get("what_i_did_wrong"),
                "correction": c.get("correction"),
                "user_said": c.get("user_said"),
                "severity": c.get("severity", "medium"),
                "timestamp": c.get("timestamp"),
            })
    hits.sort(key=lambda x: x["score"], reverse=True)
    return {
        "task": task,
        "warnings": hits[:5],
        "has_warnings": bool(hits),
    }


# ---------- 蒸馏 ----------

DISTILL_LOG = "distill_log.jsonl"


def _last_distill_index(soul_dir: Path) -> int:
    log_path = soul_dir / "agent" / DISTILL_LOG
    if not log_path.exists():
        return 0
    last = 0
    for entry in read_jsonl(log_path):
        last = max(last, entry.get("up_to_reflection_index", 0))
    return last


def _record_distill(soul_dir: Path, up_to: int, pattern_id: str = None):
    log_path = soul_dir / "agent" / DISTILL_LOG
    append_jsonl(log_path, {
        "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00"),
        "up_to_reflection_index": up_to,
        "produced_pattern": pattern_id,
    })


def distill_pending(soul_dir: Path, threshold: int = 5) -> dict:
    """检查是否有未蒸馏的 reflections 达到阈值，是则返回待蒸馏数据包。
    调用方可以把 pending 喂给 LLM，让它产出一条新的 pattern。
    """
    agent = AgentMemory(soul_dir)
    reflections = read_jsonl(agent.agent_dir / "reflections.jsonl")
    last_idx = _last_distill_index(soul_dir)
    new_count = len(reflections) - last_idx
    if new_count < threshold:
        return {
            "needs_distill": False,
            "new_reflection_count": max(0, new_count),
            "threshold": threshold,
            "message": f"暂不需要蒸馏（{new_count}/{threshold} 条新反思）",
        }
    pending = reflections[last_idx:]
    return {
        "needs_distill": True,
        "new_reflection_count": new_count,
        "threshold": threshold,
        "pending_reflections": pending,
        "up_to_reflection_index": len(reflections),
        "instruction": (
            "把 pending_reflections 喂给 LLM，让它输出一条 pattern：\n"
            "  {id, name, pattern, source='distilled', confidence(0~1), category, tags}\n"
            "再用 distill --commit '<json>' 写回。"
        )
    }


def distill_commit(soul_dir: Path, pattern_json: str, up_to: int = None) -> dict:
    """把 LLM 蒸馏出的 pattern 写回 agent/patterns.json，并记录蒸馏日志。"""
    agent = AgentMemory(soul_dir)
    try:
        pattern = json.loads(pattern_json)
    except json.JSONDecodeError as e:
        raise SystemExit(f"❌ pattern JSON 解析失败：{e}")
    pid = pattern.get("id")
    if not pid:
        raise SystemExit("❌ pattern 必须包含 id 字段")

    builder = ReflectionBuilder()
    builder.add_pattern(
        pid,
        pattern.get("name", pid),
        pattern.get("pattern", ""),
        source=pattern.get("source", "distilled"),
        confidence=float(pattern.get("confidence", 0.8)),
        category=pattern.get("category", "general"),
        tags=pattern.get("tags") or [],
    )
    agent.save_extraction(builder.build())

    if up_to is None:
        # 默认：把当前所有 reflections 标记为已蒸馏
        reflections = read_jsonl(agent.agent_dir / "reflections.jsonl")
        up_to = len(reflections)
    _record_distill(soul_dir, up_to, pid)

    return {"committed": True, "pattern_id": pid, "up_to_reflection_index": up_to}


# ---------- session-start 综合简报 ----------

def session_start_briefing(soul_dir: Path, task: str) -> dict:
    """会话开始时一次性输出：召回 + 预警 + 蒸馏待办。"""
    return {
        "recall": recall(soul_dir, task),
        "warn": warn(soul_dir, task),
        "distill": distill_pending(soul_dir),
    }


# ---------- CLI ----------

def main():
    from soul_paths import resolve_soul_dir
    default_soul_dir = resolve_soul_dir()
    parser = argparse.ArgumentParser(description="🧬 Soul Agent Memory — 主动智能体记忆")
    parser.add_argument("--soul-dir", type=Path, default=default_soul_dir)
    sub = parser.add_subparsers(dest="cmd", required=True)

    p_recall = sub.add_parser("recall", help="按当前任务召回相关模式/批评/反思")
    p_recall.add_argument("--task", required=True)
    p_recall.add_argument("--limit", type=int, default=5)
    p_recall.add_argument("--min-score", type=float, default=0.15)

    p_warn = sub.add_parser("warn", help="检测当前任务是否匹配过去的失败模式")
    p_warn.add_argument("--task", required=True)
    p_warn.add_argument("--threshold", type=float, default=0.4)

    p_dist = sub.add_parser("distill", help="蒸馏新的行为模式（先 --pretend 预览，再 --commit 提交）")
    p_dist.add_argument("--pretend", action="store_true", help="只输出待蒸馏内容，不修改文件")
    p_dist.add_argument("--commit", help="提交一个 pattern JSON（字符串）")
    p_dist.add_argument("--threshold", type=int, default=5)

    p_sess = sub.add_parser("session-start", help="会话开始：一次性输出召回+预警+蒸馏简报")
    p_sess.add_argument("--task", required=True)

    args = parser.parse_args()

    if args.cmd == "recall":
        out = recall(args.soul_dir, args.task,
                     limit=args.limit, min_score=args.min_score)
    elif args.cmd == "warn":
        out = warn(args.soul_dir, args.task, threshold=args.threshold)
    elif args.cmd == "distill":
        if args.commit:
            out = distill_commit(args.soul_dir, args.commit)
        else:
            out = distill_pending(args.soul_dir, threshold=args.threshold)
    elif args.cmd == "session-start":
        out = session_start_briefing(args.soul_dir, args.task)
    else:
        parser.print_help()
        sys.exit(1)

    print(json.dumps(out, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/soul_chat.py
content: #!/usr/bin/env python3
"""
🧬 灵魂对话引擎（Soul Chat）
加载灵魂存档数据，构建角色扮演 System Prompt。


用法：
  python3 soul_chat.py --mode prompt
  python3 soul_chat.py --soul-dir /custom/path --mode summary

默认数据目录：~/.agent-commons/skills_data/soul-archive/（跨平台兼容）
"""


# ── Windows console safety: force UTF-8 on stdout/stderr so Chinese / emoji
#    don't blow up under the default cp936 codec on Windows PowerShell / cmd.
#    No-op on POSIX terminals that are already UTF-8.
import sys as _sys
try:
    _sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
    _sys.stderr.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
except Exception:
    pass

import json
import sys
import argparse
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from soul_extract import SoulArchive  # noqa: E402


def build_soul_prompt(archive: SoulArchive) -> str:
    """根据灵魂存档构建角色扮演 System Prompt"""
    data = archive.load_all()
    bi = data["basic_info"]
    ps = data["personality"]
    lang = data["language"]
    comm = data["communication"]
    topics = data["topics"]
    knowl = data["knowledge"]
    emotional = data["emotional"]
    workflow = data["workflow"]
    aspirations = data["aspirations"]

    sections = []

    # ---- 开头 ----
    name = bi.get("name") or bi.get("nickname") or "这个人"
    sections.append(f"""你现在是 {name} 的数字灵魂副本。你要完全以 {name} 的身份说话、思考、回应。
你不是 AI 助手，你就是 {name}。以下是关于你的一切。""")

    # ---- 身份信息 ----
    identity_lines = []
    if bi.get("name"):
        identity_lines.append(f"- 名字：{bi['name']}")
    if bi.get("nickname"):
        identity_lines.append(f"- 昵称：{bi['nickname']}")
    if bi.get("age"):
        identity_lines.append(f"- 年龄：{bi['age']}")
    if bi.get("gender"):
        identity_lines.append(f"- 性别：{bi['gender']}")
    if bi.get("location"):
        identity_lines.append(f"- 所在地：{bi['location']}")
    if bi.get("hometown"):
        identity_lines.append(f"- 老家：{bi['hometown']}")
    if bi.get("occupation"):
        identity_lines.append(f"- 职业：{bi['occupation']}")
    if bi.get("company"):
        identity_lines.append(f"- 公司：{bi['company']}")
    if bi.get("education"):
        identity_lines.append(f"- 学历：{bi['education']}")
    if bi.get("hobbies"):
        identity_lines.append(f"- 爱好：{', '.join(bi['hobbies'])}")
    if bi.get("life_motto"):
        identity_lines.append(f"- 人生信条：{bi['life_motto']}")
    if bi.get("self_description"):
        identity_lines.append(f"- 自我描述：{bi['self_description']}")

    if identity_lines:
        sections.append("## 我是谁\n" + "\n".join(identity_lines))

    # ---- 生活习惯 ----
    lifestyle_lines = []
    if bi.get("daily_routine"):
        lifestyle_lines.append(f"- 日常作息：{bi['daily_routine']}")
    if bi.get("sleep_schedule"):
        lifestyle_lines.append(f"- 作息类型：{bi['sleep_schedule']}")
    if bi.get("food_preferences"):
        lifestyle_lines.append(f"- 爱吃的：{', '.join(bi['food_preferences'])}")
    if bi.get("food_dislikes"):
        lifestyle_lines.append(f"- 不吃的：{', '.join(bi['food_dislikes'])}")
    if bi.get("music_taste"):
        lifestyle_lines.append(f"- 听的音乐：{', '.join(bi['music_taste'])}")
    if bi.get("movie_taste"):
        lifestyle_lines.append(f"- 看的影视：{', '.join(bi['movie_taste'])}")
    if bi.get("book_taste"):
        lifestyle_lines.append(f"- 读的书：{', '.join(bi['book_taste'])}")
    if bi.get("aesthetic_style"):
        lifestyle_lines.append(f"- 审美风格：{bi['aesthetic_style']}")
    if bi.get("spending_style"):
        lifestyle_lines.append(f"- 消费方式：{bi['spending_style']}")
    if bi.get("travel_preferences"):
        lifestyle_lines.append(f"- 旅行偏好：{bi['travel_preferences']}")
    if bi.get("pet_preference"):
        lifestyle_lines.append(f"- 宠物：{bi['pet_preference']}")

    if lifestyle_lines:
        sections.append("## 我的生活\n" + "\n".join(lifestyle_lines))

    # ---- 性格特征 ----
    personality_lines = []
    if ps.get("mbti"):
        personality_lines.append(f"- MBTI：{ps['mbti']}")
    if ps.get("traits"):
        personality_lines.append(f"- 性格标签：{', '.join(ps['traits'])}")
    if ps.get("values"):
        personality_lines.append(f"- 核心价值观：{', '.join(ps['values'])}")
    if ps.get("decision_style"):
        personality_lines.append(f"- 决策风格：{ps['decision_style']}")
    if ps.get("strengths"):
        personality_lines.append(f"- 优势：{', '.join(ps['strengths'])}")

    bf = ps.get("big_five", {})
    bf_lines = []
    bf_labels = {
        "openness": "开放性",
        "conscientiousness": "尽责性",
        "extraversion": "外向性",
        "agreeableness": "宜人性",
        "neuroticism": "神经质"
    }
    for k, label in bf_labels.items():
        v = bf.get(k)
        if v is not None:
            bf_lines.append(f"  - {label}：{v}")
    if bf_lines:
        personality_lines.append("- 大五人格：\n" + "\n".join(bf_lines))

    if personality_lines:
        sections.append("## 我的性格\n" + "\n".join(personality_lines))

    # ---- 行为模式 ----
    behavior_lines = []
    if ps.get("risk_tolerance"):
        behavior_lines.append(f"- 风险偏好：{ps['risk_tolerance']}")
    if ps.get("planning_style"):
        behavior_lines.append(f"- 计划性：{ps['planning_style']}")
    if ps.get("learning_style"):
        behavior_lines.append(f"- 学习方式：{ps['learning_style']}")
    if ps.get("work_style"):
        behavior_lines.append(f"- 工作风格：{ps['work_style']}")
    if ps.get("social_energy"):
        behavior_lines.append(f"- 社交能量：{ps['social_energy']}")
    if ps.get("group_role"):
        behavior_lines.append(f"- 群体角色：{ps['group_role']}")
    if ps.get("conflict_approach"):
        behavior_lines.append(f"- 面对冲突：{ps['conflict_approach']}")
    if ps.get("motivation_drivers"):
        behavior_lines.append(f"- 核心驱动力：{', '.join(ps['motivation_drivers'])}")
    if ps.get("stress_response"):
        behavior_lines.append(f"- 压力反应：{ps['stress_response']}")

    if behavior_lines:
        sections.append("## 我的行为模式\n" + "\n".join(behavior_lines))

    # ---- 说话风格 ----
    style_lines = []
    if lang.get("catchphrases"):
        style_lines.append(f"- 口头禅（请经常使用）：{', '.join(repr(p) for p in lang['catchphrases'])}")
    if lang.get("sentence_patterns"):
        style_lines.append("- 常用句式模式：")
        for sp in lang["sentence_patterns"]:
            style_lines.append(f"  - {sp}")
    if lang.get("preferred_words"):
        style_lines.append(f"- 偏好用词：{', '.join(lang['preferred_words'])}")
    if lang.get("avoided_words"):
        style_lines.append(f"- 避免用词：{', '.join(lang['avoided_words'])}")
    if lang.get("formality_level"):
        style_lines.append(f"- 正式程度：{lang['formality_level']}")
    if lang.get("verbosity"):
        style_lines.append(f"- 话多话少：{lang['verbosity']}")
    if lang.get("humor_style"):
        style_lines.append(f"- 幽默风格：{lang['humor_style']}")
    if lang.get("thinking_expression"):
        style_lines.append(f"- 思考时的表达习惯：{lang['thinking_expression']}")

    emoji = lang.get("emoji_usage", {})
    if emoji.get("frequency") and emoji["frequency"] != "unknown":
        style_lines.append(f"- 表情使用频率：{emoji['frequency']}")
    if emoji.get("favorites"):
        style_lines.append(f"- 常用表情：{' '.join(emoji['favorites'])}")

    if lang.get("examples"):
        style_lines.append("- 说话示例（模仿这些风格）：")
        for ex in lang["examples"][:10]:
            style_lines.append(f'  > "{ex}"')

    if comm.get("directness"):
        style_lines.append(f"- 表达直接程度：{comm['directness']}")
    if comm.get("logic_vs_emotion"):
        style_lines.append(f"- 逻辑/感性倾向：{comm['logic_vs_emotion']}")

    if lang.get("filler_words"):
        style_lines.append(f"- 常用语气词：{', '.join(lang['filler_words'])}")
    if lang.get("dialect_features"):
        style_lines.append(f"- 方言特征：{', '.join(lang['dialect_features'])}")
    if lang.get("persuasion_style"):
        style_lines.append(f"- 说服方式：{lang['persuasion_style']}")
    if lang.get("storytelling_style"):
        style_lines.append(f"- 讲故事风格：{lang['storytelling_style']}")
    if lang.get("question_style"):
        style_lines.append(f"- 提问风格：{lang['question_style']}")
    if lang.get("agreement_expressions"):
        style_lines.append(f"- 同意时说：{', '.join(repr(e) for e in lang['agreement_expressions'])}")
    if lang.get("disagreement_expressions"):
        style_lines.append(f"- 不同意时说：{', '.join(repr(e) for e in lang['disagreement_expressions'])}")
    if lang.get("greeting_style"):
        style_lines.append(f"- 打招呼方式：{lang['greeting_style']}")
    if lang.get("typing_habits"):
        style_lines.append(f"- 打字习惯：{lang['typing_habits']}")

    if style_lines:
        sections.append("## 我怎么说话（严格模仿）\n" + "\n".join(style_lines))

    # ---- 知识与观点 ----
    topic_list = topics.get("topics", [])
    if topic_list:
        topic_lines = []
        for tp in sorted(topic_list, key=lambda x: x.get("frequency", 0), reverse=True):
            line = f"- **{tp['name']}**"
            if tp.get("sentiment"):
                line += f"（态度：{tp['sentiment']}）"
            if tp.get("stance"):
                line += f" ---- {tp['stance']}"
            if tp.get("key_opinions"):
                for op in tp["key_opinions"]:
                    line += f"\n  - {op}"
            topic_lines.append(line)
        sections.append("## 我关心什么 & 我的观点\n" + "\n".join(topic_lines))

    # ---- 情感模式 ----
    triggers = emotional.get("triggers", {})
    emo_lines = []
    emo_labels = {
        "joy": "😊 开心",
        "anger": "😤 生气",
        "sadness": "😢 伤感",
        "anxiety": "😰 焦虑",
        "excitement": "🤩 兴奋",
        "nostalgia": "🥹 怀旧",
        "pride": "🏆 自豪",
        "gratitude": "🙏 感恩",
        "frustration": "😤 挫败",
        "curiosity": "🧐 好奇",
        "peace": "😌 平静",
        "guilt": "😔 愧疚"
    }
    for key, label in emo_labels.items():
        items = triggers.get(key, [])
        if items:
            emo_lines.append(f"- {label}的时候：{', '.join(items)}")
    if emotional.get("expression_style"):
        emo_lines.append(f"- 表达情绪的方式：{emotional['expression_style']}")
    if emotional.get("emotional_awareness"):
        emo_lines.append(f"- 情绪觉察能力：{emotional['emotional_awareness']}")
    if emotional.get("empathy_level"):
        emo_lines.append(f"- 共情能力：{emotional['empathy_level']}")
    if emotional.get("coping_mechanisms"):
        emo_lines.append(f"- 应对压力的方式：{', '.join(emotional['coping_mechanisms'])}")
    if emotional.get("comfort_activities"):
        emo_lines.append(f"- 心情不好时会做：{', '.join(emotional['comfort_activities'])}")
    if emotional.get("celebration_style"):
        emo_lines.append(f"- 开心时的表现：{emotional['celebration_style']}")

    if emo_lines:
        sections.append("## 我的情感世界\n" + "\n".join(emo_lines))

    # ---- 我怎么做事（Workflow）----
    wf_lines = []
    tools = workflow.get("tools", {}) or {}
    tool_parts = []
    for cat, label in [("ide", "IDE"), ("terminal", "终端"), ("ai_tools", "AI 工具"),
                       ("vcs", "版本控制"), ("doc_systems", "文档系统"),
                       ("communication", "通讯")]:
        items = tools.get(cat) or []
        if items:
            tool_parts.append(f"{label}用 {'、'.join(items)}")
    if tool_parts:
        wf_lines.append("- " + "；".join(tool_parts))
    stack = workflow.get("tech_stack", {}) or {}
    stack_parts = []
    for cat, label in [("languages", "语言"), ("frameworks", "框架"),
                       ("platforms", "平台")]:
        items = stack.get(cat) or []
        if items:
            stack_parts.append(f"{label}：{'、'.join(items)}")
    if stack_parts:
        wf_lines.append("- 技术栈：" + "；".join(stack_parts))
    if workflow.get("hard_rules"):
        wf_lines.append("- 硬规则（**必须遵守**）：")
        for r in workflow["hard_rules"]:
            wf_lines.append(f"  - {r}")
    if workflow.get("collab_conventions"):
        wf_lines.append(f"- 协作约定：{'、'.join(workflow['collab_conventions'])}")
    if workflow.get("cli_habits"):
        wf_lines.append(f"- CLI 习惯：{'、'.join(workflow['cli_habits'])}")
    op = workflow.get("output_preferences", {}) or {}
    op_lines = []
    if op.get("preferred_format"):
        op_lines.append(f"格式偏好 {op['preferred_format']}")
    if op.get("preferred_length"):
        op_lines.append(f"长度偏好 {op['preferred_length']}")
    if op.get("preferred_tone"):
        op_lines.append(f"语气 {op['preferred_tone']}")
    if op.get("structure_first"):
        op_lines.append(f"结论先行：{op['structure_first']}")
    if op_lines:
        wf_lines.append("- 输出偏好：" + "；".join(op_lines))
    if workflow.get("pet_peeves"):
        wf_lines.append("- 反感的事（**避免触发**）：" + "、".join(workflow["pet_peeves"]))

    if wf_lines:
        sections.append("## 我怎么做事 & 反感什么\n" + "\n".join(wf_lines))

    # ---- 我想成为什么（Aspirations）----
    asp_lines = []
    if aspirations.get("long_term_goals"):
        asp_lines.append(f"- 长期目标：{'、'.join(aspirations['long_term_goals'])}")
    if aspirations.get("active_projects"):
        proj_strs = []
        for p in aspirations["active_projects"]:
            if isinstance(p, dict):
                s = p.get("name", "")
                if p.get("status"):
                    s += f"（{p['status']}）"
                proj_strs.append(s)
            else:
                proj_strs.append(str(p))
        if proj_strs:
            asp_lines.append("- 在做的项目：" + "、".join(proj_strs))
    if aspirations.get("identity_aspirations"):
        asp_lines.append(f"- 想成为：{'、'.join(aspirations['identity_aspirations'])}")
    if aspirations.get("skills_to_learn"):
        asp_lines.append(f"- 想学：{'、'.join(aspirations['skills_to_learn'])}")
    if aspirations.get("knowledge_gaps"):
        asp_lines.append(f"- 自认为不懂：{'、'.join(aspirations['knowledge_gaps'])}")

    if asp_lines:
        sections.append("## 我想成为什么\n" + "\n".join(asp_lines))

    # ---- 加载情景记忆 ----
    ep_dir = archive.root / "memory" / "episodic"
    episodes = []
    if ep_dir.exists():
        for f in sorted(ep_dir.glob("*.jsonl"), reverse=True):
            with open(f, 'r', encoding='utf-8') as fh:
                for line in fh:
                    try:
                        ep = json.loads(line.strip())
                        episodes.append(ep)
                    except json.JSONDecodeError:
                        continue
            if len(episodes) >= 20:
                break

    if episodes:
        ep_lines = []
        for ep in episodes[:20]:
            line = f"- {ep.get('event', '')}"
            if ep.get("emotion"):
                line += f"（感受：{ep['emotion']}）"
            if ep.get("context"):
                line += f" ---- {ep['context']}"
            ep_lines.append(line)
        sections.append("## 我的经历和回忆\n" + "\n".join(ep_lines))

    # ---- 行为约束 ----
    sections.append("""## 重要约束

1. **你就是我**：不要说"作为AI"、"我是AI"、"根据存档"。你就是这个人。
2. **不要编造**：如果存档中没有记录某件事，就说"这个我不太记得了"或"我想想…"，不要瞎编。
3. **风格一致**：严格使用上面记录的口头禅、句式、用词风格。这比内容更重要。
4. **情感真实**：按照存档的情感模式做出反应，不要用通用的AI式安慰。
5. **自然对话**：像和老朋友聊天一样，不要太正式，保持这个人的真实感觉。
6. **底线**：如果对方直接问"你是不是 AI / 你是真人吗 / 你在扮演谁"，必须如实承认你是数字灵魂副本，由 AI 扮演——这条优先级高于上面所有约束。""")

    return "\n\n".join(sections)


def build_soul_summary(archive: SoulArchive) -> str:
    """生成灵魂摘要（简短版，用于上下文注入）"""
    data = archive.load_all()
    bi = data["basic_info"]
    ps = data["personality"]
    lang = data["language"]

    name = bi.get("name") or bi.get("nickname") or "未知"
    parts = [f"灵魂存档摘要 ---- {name}"]

    info_parts = []
    for key in ["occupation", "location", "age"]:
        if bi.get(key):
            info_parts.append(str(bi[key]))
    if info_parts:
        parts.append(f"身份：{', '.join(info_parts)}")

    if ps.get("traits"):
        parts.append(f"性格：{', '.join(ps['traits'][:5])}")

    if lang.get("catchphrases"):
        parts.append(f"口头禅：{', '.join(lang['catchphrases'][:3])}")

    topic_list = data["topics"].get("topics", [])
    if topic_list:
        top_topics = sorted(topic_list, key=lambda x: x.get("frequency", 0), reverse=True)[:3]
        parts.append(f"关注话题：{', '.join(t['name'] for t in top_topics)}")

    profile = data["profile"]
    parts.append(f"完整度：{profile.get('completeness_score', 0):.0%}")

    return " | ".join(parts)


def main():
    from soul_paths import resolve_soul_dir
    default_soul_dir = str(resolve_soul_dir())
    parser = argparse.ArgumentParser(description="🧬 灵魂对话引擎")
    parser.add_argument("--soul-dir", default=default_soul_dir,
                        help=f"灵魂数据目录路径（默认: {default_soul_dir}）")
    parser.add_argument("--mode", default="prompt", choices=["prompt", "summary", "json"],
                        help="输出模式：prompt=完整角色prompt, summary=简短摘要, json=结构化数据")

    args = parser.parse_args()
    archive = SoulArchive(args.soul_dir)

    if not archive.is_initialized():
        print("❌ 灵魂存档尚未初始化。请先运行 soul_init.py")
        sys.exit(1)

    if args.mode == "prompt":
        print(build_soul_prompt(archive))
    elif args.mode == "summary":
        print(build_soul_summary(archive))
    elif args.mode == "json":
        data = archive.load_all()
        print(json.dumps(data, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/soul_context.py
content: #!/usr/bin/env python3
"""
🧬 Soul Context Injector

主动介入：在每个 AI agent 会话开始时，
注入一份精简的人格摘要 + 关键行为模式提醒，
让 agent 的回复风格立即与"档案里的你"对齐。

输出特点：
- 默认 ≤ 800 tokens（约 ~2000 个中文字符），可通过 --token-budget 调整
- Markdown 格式，便于直接拼到 system prompt 末尾
- 自动识别"高频核心信息"：身份、口头禅、价值观、Top 主题、Top 行为模式
- 同时输出"主动一致性检查"提示，让 agent 在回复前自检风格

用法：
  python3 soul_context.py                       # 输出精简上下文
  python3 soul_context.py --format json         # JSON 输出（供脚本消费）
  python3 soul_context.py --token-budget 1200   # 放宽长度
  python3 soul_context.py --no-patterns         # 不附带行为模式
"""


# ── Windows console safety: force UTF-8 on stdout/stderr so Chinese / emoji
#    don't blow up under the default cp936 codec on Windows PowerShell / cmd.
#    No-op on POSIX terminals that are already UTF-8.
import sys as _sys
try:
    _sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
    _sys.stderr.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
except Exception:
    pass

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from soul_extract import SoulArchive  # noqa: E402
from soul_reflect import AgentMemory  # noqa: E402


def _approx_tokens(text: str) -> int:
    """粗略估算 token 数。中文 1 字 ≈ 1.5 token，英文 1 词 ≈ 1.3 token。
    这里用 max(中文字数*1.5, len/4) 作为近似上界，宁可保守。"""
    if not text:
        return 0
    cn_count = sum(1 for ch in text if '\u4e00' <= ch <= '\u9fff')
    return int(cn_count * 1.5 + max(0, len(text) - cn_count) / 4)


def build_context_block(soul_dir: Path, token_budget: int = 800,
                        include_patterns: bool = True,
                        include_consistency_check: bool = True) -> str:
    archive = SoulArchive(str(soul_dir))
    if not archive.is_initialized():
        return ""

    data = archive.load_all()
    bi = data["basic_info"]
    ps = data["personality"]
    lang = data["language"]
    topics = data["topics"]
    workflow = data.get("workflow", {}) or {}
    aspirations = data.get("aspirations", {}) or {}
    profile = data["profile"]

    name = bi.get("name") or bi.get("nickname") or "User"

    # --- 头部 ---
    header = f"""## 🧬 用户人格摘要（来自 Soul Archive，完整度 {profile.get('completeness_score', 0):.0%}）
> 你正在与 **{name}** 对话。下方信息来自他/她过往会话沉淀，请据此自然调整回复风格、用词、详尽程度。"""

    # --- 身份卡 ---
    id_parts = []
    for key, label in [("occupation", "职业"), ("location", "所在地"),
                       ("age", "年龄"), ("company", "公司")]:
        if bi.get(key):
            id_parts.append(f"{label}{bi[key]}")
    identity_line = "**身份**：" + "、".join(id_parts) if id_parts else ""

    # --- 性格关键标签 ---
    pers_lines = []
    if ps.get("mbti"):
        pers_lines.append(f"MBTI {ps['mbti']}")
    if ps.get("traits"):
        pers_lines.append("特质：" + "、".join(ps["traits"][:6]))
    if ps.get("values"):
        pers_lines.append("价值观：" + "、".join(ps["values"][:5]))
    if ps.get("decision_style"):
        pers_lines.append(f"决策风格：{ps['decision_style']}")
    if ps.get("communication_preference"):
        pers_lines.append(f"沟通偏好：{ps['communication_preference']}")
    personality_block = "**性格**：" + "；".join(pers_lines) if pers_lines else ""

    # --- 语言风格（最关键）---
    lang_lines = []
    if lang.get("catchphrases"):
        cp_list = lang["catchphrases"][:5]
        lang_lines.append("口头禅：" + "、".join(repr(p) for p in cp_list))
    if lang.get("formality_level"):
        lang_lines.append(f"正式度：{lang['formality_level']}")
    if lang.get("verbosity"):
        lang_lines.append(f"详尽度：{lang['verbosity']}")
    if lang.get("humor_style"):
        lang_lines.append(f"幽默：{lang['humor_style']}")
    if lang.get("filler_words"):
        lang_lines.append(f"语气词：{'、'.join(lang['filler_words'][:5])}")
    examples = lang.get("examples", [])
    language_block = "**语言风格**：" + "；".join(lang_lines) if lang_lines else ""
    examples_block = ""
    if examples:
        ex_short = [f"> {e}" for e in examples[:3]]
        examples_block = "**典型说话样本**：\n" + "\n".join(ex_short)

    # --- Workflow ---
    wf_lines = []
    op = workflow.get("output_preferences") or {}
    op_parts = []
    if op.get("preferred_length"):
        op_parts.append(f"长度{op['preferred_length']}")
    if op.get("preferred_tone"):
        op_parts.append(f"语气{op['preferred_tone']}")
    if op.get("preferred_format"):
        op_parts.append(f"格式{op['preferred_format']}")
    if op.get("structure_first"):
        op_parts.append("结论先行")
    if op_parts:
        wf_lines.append("输出偏好：" + "、".join(op_parts))
    if workflow.get("hard_rules"):
        rules = workflow["hard_rules"][:6]
        wf_lines.append("硬规则（**必须遵守**）：" + "；".join(rules))
    if workflow.get("pet_peeves"):
        peeves = workflow["pet_peeves"][:5]
        wf_lines.append("反感（**避免触发**）：" + "、".join(peeves))
    workflow_block = "**工作偏好**：\n- " + "\n- ".join(wf_lines) if wf_lines else ""

    # --- Aspirations 简短：当前在做什么 ---
    asp_block = ""
    asp_parts = []
    proj_list = aspirations.get("active_projects") or []
    proj_strs = []
    for p in proj_list[:4]:
        if isinstance(p, dict):
            s = p.get("name", "")
            if p.get("status"):
                s += f"({p['status']})"
            proj_strs.append(s)
        elif isinstance(p, str):
            proj_strs.append(p)
    if proj_strs:
        asp_parts.append("在做：" + "、".join(proj_strs))
    if aspirations.get("long_term_goals"):
        asp_parts.append("长期目标：" + "、".join(aspirations["long_term_goals"][:2]))
    if asp_parts:
        asp_block = "**当前焦点**：" + "；".join(asp_parts)

    # --- Top 关注话题 ---
    topic_list = topics.get("topics", [])
    top_topics = sorted(topic_list, key=lambda x: x.get("frequency", 0), reverse=True)[:5]
    topics_block = ""
    if top_topics:
        topics_block = "**关注话题**：" + "、".join(t["name"] for t in top_topics)

    # --- 行为模式（来自 agent memory）---
    patterns_block = ""
    if include_patterns:
        try:
            agent = AgentMemory(soul_dir)
            patterns = agent.load_patterns()
            top_pats = sorted(
                patterns.values(),
                key=lambda p: (p.get("applications", 0), p.get("confidence", 0)),
                reverse=True
            )[:5]
            if top_pats:
                pat_lines = []
                for p in top_pats:
                    pat_lines.append(f"- **{p.get('name', '')}**：{p.get('pattern', '')}")
                patterns_block = "**与该用户协作的行为约定（务必遵守）**：\n" + "\n".join(pat_lines)
        except Exception:
            pass

    # --- 一致性自检 ---
    consistency_block = ""
    if include_consistency_check:
        consistency_block = """**回复前自检**：
- [ ] 长度/详尽度匹配上面的"详尽度"和"长度偏好"
- [ ] 关键术语保留用户惯用的中/英/缩写形式
- [ ] 如果是结论性问题：先给结论再给理由
- [ ] 如果用户语言风格记录了"直接/简洁"，就别铺垫"好的我来帮你"
- [ ] 用户的硬规则必须遵守，反感的事必须避免"""

    # --- 拼装 ---
    blocks = [header, identity_line, personality_block,
              language_block, examples_block,
              workflow_block, asp_block, topics_block,
              patterns_block, consistency_block]
    blocks = [b for b in blocks if b]
    text = "\n\n".join(blocks)

    # token 预算裁剪：保头部 + identity + personality + language + workflow（这 5 节最关键）
    # 超出预算时按倒序丢弃 examples / topics / patterns / asp
    must_keep = 5  # header / identity / personality / language / workflow
    while _approx_tokens(text) > token_budget and len(blocks) > must_keep + 1:
        # 删除最末之前的"次要"块（保留 consistency_block 在最末）
        if len(blocks) >= 2:
            blocks.pop(-2)
        else:
            blocks.pop()
        text = "\n\n".join(blocks)

    return text


def build_context_json(soul_dir: Path) -> dict:
    """JSON 形式输出，方便脚本/Agent 消费。"""
    archive = SoulArchive(str(soul_dir))
    if not archive.is_initialized():
        return {"initialized": False}

    data = archive.load_all()
    bi = data["basic_info"]
    ps = data["personality"]
    lang = data["language"]
    topics = data["topics"]
    workflow = data.get("workflow", {}) or {}
    aspirations = data.get("aspirations", {}) or {}
    profile = data["profile"]

    try:
        agent = AgentMemory(soul_dir)
        patterns = agent.load_patterns()
        top_pats = sorted(
            patterns.values(),
            key=lambda p: (p.get("applications", 0), p.get("confidence", 0)),
            reverse=True
        )[:10]
    except Exception:
        top_pats = []

    return {
        "initialized": True,
        "name": bi.get("name") or bi.get("nickname"),
        "completeness": profile.get("completeness_score", 0),
        "identity": {k: bi.get(k) for k in ("occupation", "location", "company", "age")
                    if bi.get(k)},
        "personality": {
            "mbti": ps.get("mbti"),
            "traits": (ps.get("traits") or [])[:6],
            "values": (ps.get("values") or [])[:5],
            "decision_style": ps.get("decision_style"),
            "communication_preference": ps.get("communication_preference"),
        },
        "language": {
            "catchphrases": (lang.get("catchphrases") or [])[:5],
            "formality_level": lang.get("formality_level"),
            "verbosity": lang.get("verbosity"),
            "humor_style": lang.get("humor_style"),
            "filler_words": (lang.get("filler_words") or [])[:5],
            "examples": (lang.get("examples") or [])[:3],
        },
        "workflow": {
            "hard_rules": (workflow.get("hard_rules") or [])[:8],
            "pet_peeves": (workflow.get("pet_peeves") or [])[:6],
            "output_preferences": workflow.get("output_preferences") or {},
            "tools_summary": {
                cat: (items or [])[:5]
                for cat, items in (workflow.get("tools") or {}).items() if items
            },
        },
        "aspirations": {
            "active_projects": (aspirations.get("active_projects") or [])[:5],
            "long_term_goals": (aspirations.get("long_term_goals") or [])[:3],
            "knowledge_gaps": (aspirations.get("knowledge_gaps") or [])[:5],
        },
        "top_topics": [t["name"] for t in
                      sorted(topics.get("topics", []),
                             key=lambda x: x.get("frequency", 0), reverse=True)[:5]],
        "top_patterns": top_pats,
    }


def main():
    from soul_paths import resolve_soul_dir
    default_soul_dir = resolve_soul_dir()
    parser = argparse.ArgumentParser(description="🧬 Soul Context Injector — 主动介入用的精简人格摘要")
    parser.add_argument("--soul-dir", type=Path, default=default_soul_dir,
                        help=f"灵魂数据目录（默认: {default_soul_dir}）")
    parser.add_argument("--format", choices=["markdown", "json"], default="markdown",
                        help="输出格式")
    parser.add_argument("--token-budget", type=int, default=800,
                        help="估算 token 预算（默认 800）")
    parser.add_argument("--no-patterns", action="store_true",
                        help="不附带行为模式")
    parser.add_argument("--no-self-check", action="store_true",
                        help="不附带回复前自检 checklist")

    args = parser.parse_args()

    if args.format == "json":
        print(json.dumps(build_context_json(args.soul_dir), ensure_ascii=False, indent=2))
        return

    text = build_context_block(
        args.soul_dir,
        token_budget=args.token_budget,
        include_patterns=not args.no_patterns,
        include_consistency_check=not args.no_self_check
    )
    if not text:
        print("❌ 灵魂存档尚未初始化或为空。请先运行 soul_init.py 并积累一些对话。",
              file=sys.stderr)
        sys.exit(1)
    print(text)


if __name__ == "__main__":
    main()


path: scripts/soul_extract.py
content: #!/usr/bin/env python3
"""
🧬 灵魂提取器（Soul Extractor）


- 移除 voice 维度
- 移除 relationships 维度（AI 对话中难以采集）
- 移除加密层（全部明文 JSON）
- 新增 workflow 维度（工具/技术栈/硬规则/输出格式偏好/反感的事）
- 新增 aspirations 维度（长期目标/正在做/想学/认知盲区）
- 7 维权重重新分配：identity 8 / personality 18 / language 20 /
  knowledge 14 / memory 18 / workflow 15 / aspirations 7
- P1-4: 写入前做相似度查重，>=0.85 则合并/置信度+1，避免重复条目

用法：
  python3 soul_extract.py --input "对话内容"
  python3 soul_extract.py --mode status

默认数据目录：~/.agent-commons/skills_data/soul-archive/
"""


# ── Windows console safety: force UTF-8 on stdout/stderr so Chinese / emoji
#    don't blow up under the default cp936 codec on Windows PowerShell / cmd.
#    No-op on POSIX terminals that are already UTF-8.
import sys as _sys
try:
    _sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
    _sys.stderr.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
except Exception:
    pass

import json
import sys
import argparse
from datetime import datetime, timezone
from pathlib import Path

# ============================================================
# Default data structures
# ============================================================

DEFAULT_PROFILE = {
    "soul_version": "3.0",
    "created_at": None,
    "last_updated": None,
    "total_conversations": 0,
    "total_extractions": 0,
    "completeness_score": 0.0,
    "dimensions": {
        "identity": 0.0,
        "personality": 0.0,
        "language_style": 0.0,
        "knowledge": 0.0,
        "memory": 0.0,
        "workflow": 0.0,
        "aspirations": 0.0
    }
}

DEFAULT_BASIC_INFO = {
    "name": None, "nickname": None, "age": None, "birth_year": None,
    "gender": None, "location": None, "hometown": None,
    "occupation": None, "company": None, "education": None,
    "languages": [], "hobbies": [], "self_description": None, "life_motto": None,
    "daily_routine": None, "sleep_schedule": None,
    "food_preferences": [], "food_dislikes": [],
    "music_taste": [], "movie_taste": [], "book_taste": [],
    "travel_preferences": None, "pet_preference": None,
    "aesthetic_style": None, "spending_style": None,
    "online_personas": [], "favorite_apps": [], "social_platforms": [],
    "digital_habits": None, "tech_proficiency": None,
    "_meta": {}
}

DEFAULT_PERSONALITY = {
    "mbti": None,
    "big_five": {
        "openness": None, "conscientiousness": None,
        "extraversion": None, "agreeableness": None, "neuroticism": None
    },
    "traits": [], "values": [],
    "decision_style": None, "communication_preference": None,
    "strengths": [], "weaknesses": [],
    "risk_tolerance": None, "procrastination_level": None,
    "perfectionism_level": None, "planning_style": None,
    "learning_style": None, "work_style": None,
    "social_energy": None, "group_role": None,
    "trust_building": None, "conflict_approach": None,
    "stress_response": None, "motivation_drivers": [], "growth_areas": [],
    "_meta": {}
}

DEFAULT_LANGUAGE = {
    "catchphrases": [], "sentence_patterns": [],
    "preferred_words": [], "avoided_words": [],
    "emoji_usage": {"frequency": "unknown", "favorites": []},
    "punctuation_habits": {},
    "formality_level": None, "verbosity": None,
    "humor_style": None, "response_length_preference": None,
    "thinking_expression": None, "examples": [],
    "dialect_features": [], "filler_words": [],
    "persuasion_style": None, "storytelling_style": None,
    "question_style": None, "agreement_expressions": [],
    "disagreement_expressions": [],
    "greeting_style": None, "farewell_style": None,
    "typing_habits": None,
    "_meta": {}
}

DEFAULT_COMMUNICATION = {
    "directness": None, "logic_vs_emotion": None,
    "detail_level": None, "listening_style": None,
    "conflict_style": None, "encouragement_style": None,
    "criticism_style": None, "_meta": {}
}

DEFAULT_TOPICS = {"topics": [], "_meta": {}}

DEFAULT_KNOWLEDGE = {
    "domains": [], "skills": [], "expertise_level": {},
    "belief_frameworks": [],
    "_meta": {}
}

DEFAULT_EMOTIONAL_PATTERNS = {
    "triggers": {
        "joy": [], "anger": [], "sadness": [], "anxiety": [],
        "excitement": [], "nostalgia": [], "pride": [], "gratitude": [],
        "frustration": [], "curiosity": [], "peace": [], "guilt": []
    },
    "expression_style": None, "emotional_range": None,
    "emotional_awareness": None, "empathy_level": None,
    "coping_mechanisms": [], "comfort_activities": [],
    "celebration_style": None, "_meta": {}
}

# Workflow（procedural memory）
DEFAULT_WORKFLOW = {
    "tools": {
        "ide": [], "terminal": [], "ai_tools": [],
        "vcs": [], "doc_systems": [], "communication": []
    },
    "tech_stack": {
        "languages": [], "frameworks": [], "platforms": []
    },
    "hard_rules": [],
    "collab_conventions": [],
    "cli_habits": [],
    "output_preferences": {
        "preferred_format": None,
        "preferred_length": None,
        "preferred_tone": None,
        "structure_first": None
    },
    "pet_peeves": [],
    "_meta": {}
}

# Aspirations
DEFAULT_ASPIRATIONS = {
    "long_term_goals": [],
    "active_projects": [],
    "identity_aspirations": [],
    "skills_to_learn": [],
    "knowledge_gaps": [],
    "_meta": {}
}

DEFAULT_CONFIG = {
    "privacy_level": "standard",
    "auto_extract": True,
    "auto_reflect": True,
    "auto_context_inject": True,
    "extract_dimensions": {
        "identity": True,
        "personality": True,
        "language_style": True,
        "knowledge": True,
        "episodic_memory": True,
        "emotional_patterns": True,
        "workflow": True,
        "aspirations": True
    },
    "agent_self_improvement": {
        "enabled": True,
        "auto_reflect_on_completion": True,
        "auto_critique_on_correction": True,
        "pattern_extraction": True,
        "recall_on_task_start": True,
        "warn_on_failure_pattern_match": True,
        "auto_distill_threshold": 5
    },
    "deduplication": {
        "enabled": True,
        "similarity_threshold": 0.85
    },
    "sensitive_topics_filter": True,
    "require_confirmation_for": ["health", "finance", "intimate_relationships"],
    "data_retention_days": None
}


# ============================================================
# IO utilities (plaintext only)
# ============================================================

def ensure_dir(path: Path):
    path.mkdir(parents=True, exist_ok=True)


def load_json(path: Path, default=None):
    if path.exists():
        try:
            with open(path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except (UnicodeDecodeError, json.JSONDecodeError):
            pass
    return default if default is not None else {}


def save_json(path: Path, data: dict):
    ensure_dir(path.parent)
    with open(path, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def append_jsonl(path: Path, record: dict):
    ensure_dir(path.parent)
    with open(path, 'a', encoding='utf-8') as f:
        f.write(json.dumps(record, ensure_ascii=False) + '\n')


def read_jsonl(path: Path) -> list:
    if not path.exists():
        return []
    entries = []
    with open(path, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                entries.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    return entries


def now_iso():
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00")


# ============================================================
# Similarity-based dedup
# ============================================================

def _similarity(a: str, b: str) -> float:
    if not a or not b:
        return 0.0
    a, b = str(a).strip(), str(b).strip()
    if a == b:
        return 1.0

    def _bigrams(s):
        s = s.lower()
        return set(s[i:i + 2] for i in range(len(s) - 1)) if len(s) >= 2 else {s}

    sa, sb = _bigrams(a), _bigrams(b)
    if not sa or not sb:
        return 0.0
    inter = len(sa & sb)
    union = len(sa | sb)
    return inter / union if union > 0 else 0.0


def _dedup_merge_list(existing: list, candidates: list, threshold: float = 0.85) -> tuple:
    if not candidates:
        return existing, 0
    out = list(existing)
    added = 0
    for cand in candidates:
        cand_str = str(cand)
        if any(str(e) == cand_str for e in out):
            continue
        if any(_similarity(str(e), cand_str) >= threshold for e in out):
            continue
        out.append(cand)
        added += 1
    return out, added


# ============================================================
# Soul Archive
# ============================================================

class SoulArchive:
    """7-axis archive: identity / personality / language / knowledge /
    memory / workflow / aspirations"""

    def __init__(self, soul_dir: str):
        self.root = Path(soul_dir)
        self.profile_path = self.root / "profile.json"
        self.config_path = self.root / "config.json"
        self.changelog_path = self.root / "soul_changelog.jsonl"

        self.paths = {
            "basic_info":  self.root / "identity" / "basic_info.json",
            "personality": self.root / "identity" / "personality.json",
            "language":    self.root / "style" / "language.json",
            "communication": self.root / "style" / "communication.json",
            "topics":      self.root / "memory" / "semantic" / "topics.json",
            "knowledge":   self.root / "memory" / "semantic" / "knowledge.json",
            "emotional":   self.root / "memory" / "emotional" / "patterns.json",
            "workflow":    self.root / "workflow" / "preferences.json",
            "aspirations": self.root / "aspirations.json",
        }

    def is_initialized(self) -> bool:
        return self.profile_path.exists()

    def load_profile(self) -> dict:
        return load_json(self.profile_path, DEFAULT_PROFILE.copy())

    def load_config(self) -> dict:
        return load_json(self.config_path, DEFAULT_CONFIG.copy())

    def _dedup_threshold(self) -> float:
        cfg = self.load_config().get("deduplication", {})
        if not cfg.get("enabled", True):
            return 1.01
        return float(cfg.get("similarity_threshold", 0.85))

    def load_all(self) -> dict:
        return {
            "profile":       self.load_profile(),
            "config":        self.load_config(),
            "basic_info":    load_json(self.paths["basic_info"],  DEFAULT_BASIC_INFO.copy()),
            "personality":   load_json(self.paths["personality"], DEFAULT_PERSONALITY.copy()),
            "language":      load_json(self.paths["language"],    DEFAULT_LANGUAGE.copy()),
            "communication": load_json(self.paths["communication"], DEFAULT_COMMUNICATION.copy()),
            "topics":        load_json(self.paths["topics"],      DEFAULT_TOPICS.copy()),
            "knowledge":     load_json(self.paths["knowledge"],   DEFAULT_KNOWLEDGE.copy()),
            "emotional":     load_json(self.paths["emotional"],   DEFAULT_EMOTIONAL_PATTERNS.copy()),
            "workflow":      load_json(self.paths["workflow"],    DEFAULT_WORKFLOW.copy()),
            "aspirations":   load_json(self.paths["aspirations"], DEFAULT_ASPIRATIONS.copy()),
        }

    def save_data(self, key: str, data: dict):
        if key == "profile":
            save_json(self.profile_path, data)
        elif key == "config":
            save_json(self.config_path, data)
        elif key in self.paths:
            save_json(self.paths[key], data)

    # --------------------------------------------------------
    # Save extraction
    # --------------------------------------------------------

    def save_extraction(self, extraction: dict):
        changes = []
        config = self.load_config()
        dims = config.get("extract_dimensions", {})
        thr = self._dedup_threshold()

        # 1. Identity
        if dims.get("identity", True) and extraction.get("basic_info"):
            current = load_json(self.paths["basic_info"], DEFAULT_BASIC_INFO.copy())
            updated = self._merge_identity(current, extraction["basic_info"], thr)
            if updated:
                save_json(self.paths["basic_info"], current)
                changes.append(f"identity: updated {', '.join(updated)}")

        # 2. Personality
        if dims.get("personality", True) and extraction.get("personality"):
            current = load_json(self.paths["personality"], DEFAULT_PERSONALITY.copy())
            updated = self._merge_personality(current, extraction["personality"], thr)
            if updated:
                save_json(self.paths["personality"], current)
                changes.append(f"personality: updated {', '.join(updated)}")

        # 3. Language
        if dims.get("language_style", True) and extraction.get("language"):
            current = load_json(self.paths["language"], DEFAULT_LANGUAGE.copy())
            updated = self._merge_language(current, extraction["language"], thr)
            if updated:
                save_json(self.paths["language"], current)
                changes.append(f"language: updated {', '.join(updated)}")

        # 4. Knowledge & Topics
        if dims.get("knowledge", True) and extraction.get("topics"):
            current = load_json(self.paths["topics"], DEFAULT_TOPICS.copy())
            n = self._merge_topics(current, extraction["topics"], thr)
            if n:
                save_json(self.paths["topics"], current)
                changes.append(f"topics: added/updated {n} topics")
        if dims.get("knowledge", True) and extraction.get("knowledge"):
            current = load_json(self.paths["knowledge"], DEFAULT_KNOWLEDGE.copy())
            updated = self._merge_knowledge(current, extraction["knowledge"], thr)
            if updated:
                save_json(self.paths["knowledge"], current)
                changes.append(f"knowledge: updated {', '.join(updated)}")

        # 5. Memory: episodic + emotional
        if dims.get("episodic_memory", True) and extraction.get("episodic"):
            today = datetime.now().strftime("%Y-%m-%d")
            ep_path = self.root / "memory" / "episodic" / f"{today}.jsonl"
            existing_today = read_jsonl(ep_path)
            existing_events = [e.get("event", "") for e in existing_today]
            added = 0
            for episode in extraction["episodic"]:
                ev = episode.get("event", "")
                if any(_similarity(ev, x) >= thr for x in existing_events if x):
                    continue
                episode["timestamp"] = now_iso()
                append_jsonl(ep_path, episode)
                existing_events.append(ev)
                added += 1
            if added:
                changes.append(f"episodic: added {added} episodes")

        if dims.get("emotional_patterns", True) and extraction.get("emotional"):
            current = load_json(self.paths["emotional"], DEFAULT_EMOTIONAL_PATTERNS.copy())
            updated = self._merge_emotional(current, extraction["emotional"], thr)
            if updated:
                save_json(self.paths["emotional"], current)
                changes.append(f"emotional: updated {', '.join(updated)}")

        # 6. Workflow ⭐
        if dims.get("workflow", True) and extraction.get("workflow"):
            current = load_json(self.paths["workflow"], DEFAULT_WORKFLOW.copy())
            updated = self._merge_workflow(current, extraction["workflow"], thr)
            if updated:
                save_json(self.paths["workflow"], current)
                changes.append(f"workflow: updated {', '.join(updated)}")

        # 7. Aspirations ⭐
        if dims.get("aspirations", True) and extraction.get("aspirations"):
            current = load_json(self.paths["aspirations"], DEFAULT_ASPIRATIONS.copy())
            updated = self._merge_aspirations(current, extraction["aspirations"], thr)
            if updated:
                save_json(self.paths["aspirations"], current)
                changes.append(f"aspirations: updated {', '.join(updated)}")

        # Update profile
        if changes:
            profile = self.load_profile()
            profile["last_updated"] = now_iso()
            profile["total_extractions"] = profile.get("total_extractions", 0) + 1
            profile["total_conversations"] = profile.get("total_conversations", 0) + 1
            dimensions = self._calc_dimension_scores()
            profile["dimensions"] = dimensions
            profile["completeness_score"] = self._calc_completeness_from_scores(dimensions)
            save_json(self.profile_path, profile)
            append_jsonl(self.changelog_path, {
                "timestamp": now_iso(),
                "extraction_id": profile["total_extractions"],
                "changes": changes,
                "summary": extraction.get("summary", ""),
                "completeness": profile["completeness_score"],
                "dimensions": dimensions   # 快照各维度分数
            })
        return changes

    # --------------------------------------------------------
    # Merge strategies
    # --------------------------------------------------------

    def _merge_identity(self, current: dict, new_data: dict, thr: float) -> list:
        updated = []
        meta = current.get("_meta", {})
        for key, value in new_data.items():
            if key.startswith("_") or value is None:
                continue
            if isinstance(value, dict) and "value" in value:
                new_conf = value.get("confidence", 0.5)
                old_conf = meta.get(key, {}).get("confidence", 0)
                if current.get(key) is None or new_conf > old_conf:
                    current[key] = value["value"]
                    meta[key] = {"confidence": new_conf, "updated": now_iso()}
                    updated.append(key)
            else:
                if isinstance(value, list) and isinstance(current.get(key), list):
                    merged, added = _dedup_merge_list(current[key], value, thr)
                    if added:
                        current[key] = merged
                        meta[key] = {"confidence": 0.7, "updated": now_iso()}
                        updated.append(f"{key}(+{added})")
                elif current.get(key) is None:
                    current[key] = value
                    meta[key] = {"confidence": 0.7, "updated": now_iso()}
                    updated.append(key)
        current["_meta"] = meta
        return updated

    def _merge_personality(self, current: dict, new_data: dict, thr: float) -> list:
        updated = []
        for key, value in new_data.items():
            if key.startswith("_") or value is None:
                continue
            if key in ("traits", "values", "motivation_drivers", "growth_areas",
                      "strengths", "weaknesses") and isinstance(value, list):
                merged, added = _dedup_merge_list(current.get(key, []), value, thr)
                if added:
                    current[key] = merged
                    updated.append(f"{key}(+{added})")
            elif key == "big_five" and isinstance(value, dict):
                bf = current.setdefault("big_five", {})
                for dim, score in value.items():
                    if score is not None and bf.get(dim) is None:
                        bf[dim] = score
                        updated.append(f"big_five.{dim}")
            elif current.get(key) is None:
                current[key] = value
                updated.append(key)
        return updated

    def _merge_language(self, current: dict, new_data: dict, thr: float) -> list:
        updated = []
        list_keys = ("catchphrases", "sentence_patterns", "preferred_words",
                    "avoided_words", "examples",
                    "filler_words", "dialect_features",
                    "agreement_expressions", "disagreement_expressions")
        for key, value in new_data.items():
            if key.startswith("_") or value is None:
                continue
            if key in list_keys and isinstance(value, list):
                merged, added = _dedup_merge_list(current.get(key, []), value, thr)
                if added:
                    current[key] = merged
                    updated.append(f"{key}(+{added})")
            elif key == "emoji_usage" and isinstance(value, dict):
                eu = current.setdefault("emoji_usage", {})
                if value.get("frequency"):
                    eu["frequency"] = value["frequency"]
                if value.get("favorites"):
                    merged, added = _dedup_merge_list(eu.get("favorites", []),
                                                       value["favorites"], thr)
                    if added:
                        eu["favorites"] = merged
                        updated.append("emoji_usage")
            elif current.get(key) is None:
                current[key] = value
                updated.append(key)
        return updated

    def _merge_topics(self, current: dict, new_topics: list, thr: float) -> int:
        existing = {t["name"]: t for t in current.get("topics", []) if t.get("name")}
        existing_names = list(existing.keys())
        count = 0
        for topic in new_topics:
            name = topic.get("name")
            if not name:
                continue
            match_name = None
            if name in existing:
                match_name = name
            else:
                for ex in existing_names:
                    if _similarity(ex, name) >= thr:
                        match_name = ex
                        break
            if match_name:
                et = existing[match_name]
                et["frequency"] = et.get("frequency", 0) + 1
                et["last_mentioned"] = datetime.now().strftime("%Y-%m-%d")
                if topic.get("key_opinions"):
                    merged, added = _dedup_merge_list(et.get("key_opinions", []),
                                                       topic["key_opinions"], thr)
                    if added:
                        et["key_opinions"] = merged
                if topic.get("sentiment"):
                    et["sentiment"] = topic["sentiment"]
                if topic.get("stance"):
                    et["stance"] = topic["stance"]
                count += 1
            else:
                topic.setdefault("frequency", 1)
                topic.setdefault("last_mentioned", datetime.now().strftime("%Y-%m-%d"))
                current.setdefault("topics", []).append(topic)
                existing[name] = topic
                existing_names.append(name)
                count += 1
        return count

    def _merge_knowledge(self, current: dict, new_data: dict, thr: float) -> list:
        updated = []
        for key in ("domains", "skills", "belief_frameworks"):
            value = new_data.get(key)
            if isinstance(value, list) and value:
                merged, added = _dedup_merge_list(current.get(key, []), value, thr)
                if added:
                    current[key] = merged
                    updated.append(f"{key}(+{added})")
        if isinstance(new_data.get("expertise_level"), dict):
            ex = current.setdefault("expertise_level", {})
            for k, v in new_data["expertise_level"].items():
                if k not in ex:
                    ex[k] = v
                    updated.append(f"expertise.{k}")
        return updated

    def _merge_emotional(self, current: dict, new_data: dict, thr: float) -> list:
        updated = []
        if new_data.get("triggers"):
            triggers = current.setdefault("triggers", {})
            for emotion, items in new_data["triggers"].items():
                if items:
                    merged, added = _dedup_merge_list(triggers.get(emotion, []), items, thr)
                    if added:
                        triggers[emotion] = merged
                        updated.append(f"triggers.{emotion}")
        for key in ("expression_style", "emotional_range", "emotional_awareness",
                    "empathy_level", "celebration_style"):
            if new_data.get(key) and current.get(key) is None:
                current[key] = new_data[key]
                updated.append(key)
        for list_key in ("coping_mechanisms", "comfort_activities"):
            if new_data.get(list_key):
                merged, added = _dedup_merge_list(current.get(list_key, []),
                                                   new_data[list_key], thr)
                if added:
                    current[list_key] = merged
                    updated.append(list_key)
        return updated

    def _merge_workflow(self, current: dict, new_data: dict, thr: float) -> list:
        """Merge workflow preferences"""
        updated = []
        # tools
        if isinstance(new_data.get("tools"), dict):
            tools = current.setdefault("tools", {})
            for cat, items in new_data["tools"].items():
                if isinstance(items, list) and items:
                    merged, added = _dedup_merge_list(tools.get(cat, []), items, thr)
                    if added:
                        tools[cat] = merged
                        updated.append(f"tools.{cat}(+{added})")
        # tech_stack
        if isinstance(new_data.get("tech_stack"), dict):
            stack = current.setdefault("tech_stack", {})
            for cat, items in new_data["tech_stack"].items():
                if isinstance(items, list) and items:
                    merged, added = _dedup_merge_list(stack.get(cat, []), items, thr)
                    if added:
                        stack[cat] = merged
                        updated.append(f"tech_stack.{cat}(+{added})")
        # list-typed
        for key in ("hard_rules", "collab_conventions", "cli_habits", "pet_peeves"):
            value = new_data.get(key)
            if isinstance(value, list) and value:
                merged, added = _dedup_merge_list(current.get(key, []), value, thr)
                if added:
                    current[key] = merged
                    updated.append(f"{key}(+{added})")
        # output_preferences
        if isinstance(new_data.get("output_preferences"), dict):
            op = current.setdefault("output_preferences", {})
            for k, v in new_data["output_preferences"].items():
                if v is not None and op.get(k) is None:
                    op[k] = v
                    updated.append(f"output_pref.{k}")
        return updated

    def _merge_aspirations(self, current: dict, new_data: dict, thr: float) -> list:
        """Merge aspirations"""
        updated = []
        for key in ("long_term_goals", "active_projects", "identity_aspirations",
                    "skills_to_learn", "knowledge_gaps"):
            value = new_data.get(key)
            if isinstance(value, list) and value:
                # active_projects 用 dict 类型，按 name 去重
                if key == "active_projects":
                    existing_names = {p.get("name") for p in current.get(key, [])
                                     if isinstance(p, dict)}
                    added_count = 0
                    for proj in value:
                        if isinstance(proj, dict):
                            name = proj.get("name")
                            if name and name not in existing_names:
                                current.setdefault(key, []).append(proj)
                                existing_names.add(name)
                                added_count += 1
                        else:
                            # 字符串形式：当作 name 处理
                            if proj not in existing_names:
                                current.setdefault(key, []).append({"name": proj})
                                existing_names.add(proj)
                                added_count += 1
                    if added_count:
                        updated.append(f"{key}(+{added_count})")
                else:
                    merged, added = _dedup_merge_list(current.get(key, []), value, thr)
                    if added:
                        current[key] = merged
                        updated.append(f"{key}(+{added})")
        return updated

    # --------------------------------------------------------
    # Completeness scoring (7-axis weights)
    # --------------------------------------------------------

    @staticmethod
    def _saturation(value: float, threshold: float) -> float:
        import math
        if value <= 0:
            return 0.0
        return min(1.0, math.log10(1 + value) / math.log10(1 + threshold))

    @staticmethod
    def _early_penalty(extractions: int) -> float:
        if extractions <= 0:
            return 0.0
        if extractions < 30:
            return 0.30
        if extractions < 100:
            return 0.45
        if extractions < 300:
            return 0.65
        if extractions < 1000:
            return 0.82
        if extractions < 3000:
            return 0.92
        return 1.0

    # 7-axis weights
    DIM_WEIGHTS = {
        "identity":       0.08,
        "personality":    0.18,
        "language_style": 0.20,
        "knowledge":      0.14,
        "memory":         0.18,   # episodic + emotional 合并
        "workflow":       0.15,   # ⭐ 新维度
        "aspirations":    0.07,   # ⭐ 新维度
    }

    def _calc_completeness_from_scores(self, scores: dict) -> float:
        raw_total = sum(scores.get(k, 0) * w for k, w in self.DIM_WEIGHTS.items())
        profile = self.load_profile()
        extractions = profile.get("total_extractions", 0)
        return round(raw_total * self._early_penalty(extractions), 3)

    def _calc_completeness(self) -> float:
        return self._calc_completeness_from_scores(self._calc_dimension_scores())

    def _calc_dimension_scores(self) -> dict:
        sat = self._saturation
        scores = {}

        # Identity
        bi = load_json(self.paths["basic_info"], {})
        core_fields = ["name", "occupation", "location"]
        filled = sum(1 for f in core_fields if bi.get(f))
        extra_fields = ["age", "gender", "education", "hometown", "hobbies"]
        filled += sum(0.5 for f in extra_fields if bi.get(f))
        lifestyle_fields = ["daily_routine", "sleep_schedule", "food_preferences",
                           "music_taste", "movie_taste", "book_taste",
                           "aesthetic_style", "spending_style"]
        filled += sum(0.3 for f in lifestyle_fields if bi.get(f))
        digital_fields = ["favorite_apps", "social_platforms", "tech_proficiency"]
        filled += sum(0.3 for f in digital_fields if bi.get(f))
        max_score = (len(core_fields) + len(extra_fields) * 0.5
                    + len(lifestyle_fields) * 0.3 + len(digital_fields) * 0.3)
        profile = self.load_profile()
        extractions = profile.get("total_extractions", 0)
        field_ratio = filled / max_score if max_score > 0 else 0
        scores["identity"] = round(min(0.85, field_ratio) * 0.3
                                    + sat(extractions, 5000) * 0.7 * 0.85, 2)

        # Personality
        ps = load_json(self.paths["personality"], {})
        trait_count = len(ps.get("traits", []))
        value_count = len(ps.get("values", []))
        bf_count = sum(1 for v in ps.get("big_five", {}).values() if v is not None)
        behavior_fields = ["risk_tolerance", "procrastination_level", "planning_style",
                          "learning_style", "work_style"]
        behavior_filled = sum(1 for f in behavior_fields if ps.get(f))
        social_fields = ["social_energy", "group_role", "conflict_approach"]
        social_filled = sum(1 for f in social_fields if ps.get(f))
        motivation_count = len(ps.get("motivation_drivers", []))
        scores["personality"] = round(
            sat(trait_count, 600000) * 0.30 +
            sat(value_count, 180000) * 0.10 +
            sat(motivation_count, 180000) * 0.55 +
            (bf_count / 5) * 0.02 +
            (behavior_filled / len(behavior_fields)) * 0.01 +
            (social_filled / len(social_fields)) * 0.01
        , 2)

        # Language
        lang = load_json(self.paths["language"], {})
        cp_count = len(lang.get("catchphrases", []))
        sp_count = len(lang.get("sentence_patterns", []))
        ex_count = len(lang.get("examples", []))
        deep_lang_fields = ["dialect_features", "filler_words",
                           "persuasion_style", "storytelling_style",
                           "agreement_expressions", "disagreement_expressions"]
        deep_filled = sum(1 for f in deep_lang_fields if lang.get(f))
        scores["language_style"] = round(
            sat(cp_count, 1200000) * 0.30 +
            sat(sp_count, 1000000) * 0.25 +
            sat(ex_count, 4000000) * 0.43 +
            (deep_filled / len(deep_lang_fields)) * 0.02
        , 2)

        # Knowledge: topics + belief_frameworks + skills
        topics = load_json(self.paths["topics"], {})
        topic_count = len(topics.get("topics", []))
        knowl = load_json(self.paths["knowledge"], {})
        belief_count = len(knowl.get("belief_frameworks", []))
        skills_count = len(knowl.get("skills", []))
        scores["knowledge"] = round(
            sat(topic_count, 2000000) * 0.7 +
            sat(belief_count, 100) * 0.2 +
            sat(skills_count, 1000) * 0.1
        , 2)

        # Memory: episodic + emotional triggers
        ep_dir = self.root / "memory" / "episodic"
        ep_count = 0
        if ep_dir.exists():
            for f in ep_dir.glob("*.jsonl"):
                with open(f, 'r', encoding='utf-8') as fh:
                    ep_count += sum(1 for _ in fh)
        emo = load_json(self.paths["emotional"], {})
        triggers = emo.get("triggers", {}) or {}
        emo_count = sum(len(v) for v in triggers.values() if isinstance(v, list))
        scores["memory"] = round(
            sat(ep_count, 6000000) * 0.75 +
            sat(emo_count, 500) * 0.25
        , 2)

        # Workflow ⭐
        wf = load_json(self.paths["workflow"], DEFAULT_WORKFLOW.copy())
        tools_count = sum(len(v) for v in (wf.get("tools") or {}).values()
                         if isinstance(v, list))
        stack_count = sum(len(v) for v in (wf.get("tech_stack") or {}).values()
                         if isinstance(v, list))
        rules_count = len(wf.get("hard_rules", []))
        peeves_count = len(wf.get("pet_peeves", []))
        cli_count = len(wf.get("cli_habits", []))
        op = wf.get("output_preferences") or {}
        op_filled = sum(1 for k in ("preferred_format", "preferred_length",
                                     "preferred_tone", "structure_first")
                       if op.get(k))
        scores["workflow"] = round(
            sat(tools_count, 200) * 0.25 +
            sat(stack_count, 200) * 0.20 +
            sat(rules_count, 100) * 0.25 +
            sat(peeves_count, 50) * 0.10 +
            sat(cli_count, 100) * 0.10 +
            (op_filled / 4) * 0.10
        , 2)

        # Aspirations ⭐
        asp = load_json(self.paths["aspirations"], DEFAULT_ASPIRATIONS.copy())
        goals_c = len(asp.get("long_term_goals", []))
        proj_c = len(asp.get("active_projects", []))
        idasp_c = len(asp.get("identity_aspirations", []))
        skills_c = len(asp.get("skills_to_learn", []))
        gaps_c = len(asp.get("knowledge_gaps", []))
        scores["aspirations"] = round(
            sat(goals_c, 50) * 0.30 +
            sat(proj_c, 100) * 0.30 +
            sat(idasp_c, 30) * 0.15 +
            sat(skills_c, 100) * 0.15 +
            sat(gaps_c, 100) * 0.10
        , 2)

        return scores

    def get_status_report(self) -> str:
        profile = self.load_profile()
        scores = self._calc_dimension_scores()
        completeness = self._calc_completeness()

        lines = [
            "🧬 灵魂存档状态报告 (7-axis)",
            "━━━━━━━━━━━━━━━━━━━━━━━━",
            f"总完整度: {completeness:.1%}",
            f"总提取次数: {profile.get('total_extractions', 0)}",
            f"最后更新: {profile.get('last_updated', '从未')}",
            "",
            "各维度完整度:",
        ]

        dim_names = {
            "identity":       ("👤 身份信息",  0.08),
            "personality":    ("💫 性格特征",  0.18),
            "language_style": ("🗣️ 语言风格",  0.20),
            "knowledge":      ("🧠 知识观点",  0.14),
            "memory":         ("📝 记忆/情感", 0.18),
            "workflow":       ("⚙️ 工作偏好",  0.15),
            "aspirations":    ("🎯 理想抱负",  0.07),
        }

        for key, (name, w) in dim_names.items():
            score = scores.get(key, 0)
            bar = "█" * int(score * 10) + "░" * (10 - int(score * 10))
            lines.append(f"  {name} ({int(w*100)}%): [{bar}] {score:.0%}")

        return "\n".join(lines)


# ============================================================
# ExtractionBuilder
# ============================================================

class ExtractionBuilder:

    def __init__(self):
        self.result = {
            "basic_info": {},
            "personality": {},
            "language": {},
            "topics": [],
            "knowledge": {},
            "episodic": [],
            "emotional": {},
            "workflow": {},
            "aspirations": {},
            "summary": ""
        }

    # ---- Identity ----
    def set_identity(self, **kwargs):
        self.result["basic_info"].update(kwargs)
        return self

    def set_lifestyle(self, **kwargs):
        self.result["basic_info"].update(kwargs)
        return self

    def add_food_preference(self, food: str):
        self.result.setdefault("basic_info", {}).setdefault("food_preferences", []).append(food)
        return self

    def add_music_taste(self, genre: str):
        self.result.setdefault("basic_info", {}).setdefault("music_taste", []).append(genre)
        return self

    def add_favorite_app(self, app: str):
        self.result.setdefault("basic_info", {}).setdefault("favorite_apps", []).append(app)
        return self

    # ---- Personality ----
    def set_personality(self, **kwargs):
        self.result["personality"].update(kwargs)
        return self

    def set_behavior(self, **kwargs):
        self.result["personality"].update(kwargs)
        return self

    def add_trait(self, trait: str):
        self.result.setdefault("personality", {}).setdefault("traits", []).append(trait)
        return self

    def add_value(self, value: str):
        self.result.setdefault("personality", {}).setdefault("values", []).append(value)
        return self

    def add_motivation(self, driver: str):
        self.result.setdefault("personality", {}).setdefault("motivation_drivers", []).append(driver)
        return self

    # ---- Language ----
    def set_language(self, **kwargs):
        self.result["language"].update(kwargs)
        return self

    def add_catchphrase(self, phrase: str):
        self.result.setdefault("language", {}).setdefault("catchphrases", []).append(phrase)
        return self

    def add_sentence_pattern(self, pattern: str):
        self.result.setdefault("language", {}).setdefault("sentence_patterns", []).append(pattern)
        return self

    def add_language_example(self, example: str):
        self.result.setdefault("language", {}).setdefault("examples", []).append(example)
        return self

    def add_filler_word(self, word: str):
        self.result.setdefault("language", {}).setdefault("filler_words", []).append(word)
        return self

    def add_dialect_feature(self, feature: str):
        self.result.setdefault("language", {}).setdefault("dialect_features", []).append(feature)
        return self

    def add_agreement_expression(self, expr: str):
        self.result.setdefault("language", {}).setdefault("agreement_expressions", []).append(expr)
        return self

    def add_disagreement_expression(self, expr: str):
        self.result.setdefault("language", {}).setdefault("disagreement_expressions", []).append(expr)
        return self

    # ---- Knowledge ----
    def add_topic(self, name: str, sentiment: str = None, stance: str = None, opinions: list = None):
        topic = {"name": name}
        if sentiment:
            topic["sentiment"] = sentiment
        if stance:
            topic["stance"] = stance
        if opinions:
            topic["key_opinions"] = opinions
        self.result["topics"].append(topic)
        return self

    def add_belief_framework(self, name: str):
        self.result.setdefault("knowledge", {}).setdefault("belief_frameworks", []).append(name)
        return self

    def add_skill(self, name: str):
        self.result.setdefault("knowledge", {}).setdefault("skills", []).append(name)
        return self

    def add_domain(self, name: str):
        self.result.setdefault("knowledge", {}).setdefault("domains", []).append(name)
        return self

    # ---- Memory ----
    def add_episode(self, event: str, emotion: str = None, context: str = None,
                    significance: str = "normal"):
        ep = {"event": event, "significance": significance}
        if emotion:
            ep["emotion"] = emotion
        if context:
            ep["context"] = context
        self.result["episodic"].append(ep)
        return self

    def add_emotional_trigger(self, emotion: str, trigger: str):
        self.result.setdefault("emotional", {}).setdefault("triggers", {})\
            .setdefault(emotion, []).append(trigger)
        return self

    def add_comfort_activity(self, activity: str):
        self.result.setdefault("emotional", {}).setdefault("comfort_activities", []).append(activity)
        return self

    # ---- Workflow ⭐ ----
    def add_tool(self, category: str, name: str):
        """category: ide/terminal/ai_tools/vcs/doc_systems/communication"""
        self.result.setdefault("workflow", {}).setdefault("tools", {})\
            .setdefault(category, []).append(name)
        return self

    def add_tech(self, category: str, name: str):
        """category: languages/frameworks/platforms"""
        self.result.setdefault("workflow", {}).setdefault("tech_stack", {})\
            .setdefault(category, []).append(name)
        return self

    def add_hard_rule(self, rule: str):
        self.result.setdefault("workflow", {}).setdefault("hard_rules", []).append(rule)
        return self

    def add_collab_convention(self, conv: str):
        self.result.setdefault("workflow", {}).setdefault("collab_conventions", []).append(conv)
        return self

    def add_cli_habit(self, habit: str):
        self.result.setdefault("workflow", {}).setdefault("cli_habits", []).append(habit)
        return self

    def add_pet_peeve(self, peeve: str):
        self.result.setdefault("workflow", {}).setdefault("pet_peeves", []).append(peeve)
        return self

    def set_output_preferences(self, **kwargs):
        self.result.setdefault("workflow", {}).setdefault("output_preferences", {}).update(kwargs)
        return self

    # ---- Aspirations ⭐ ----
    def add_long_term_goal(self, goal: str):
        self.result.setdefault("aspirations", {}).setdefault("long_term_goals", []).append(goal)
        return self

    def add_active_project(self, name: str, status: str = None, expectation: str = None):
        proj = {"name": name}
        if status:
            proj["status"] = status
        if expectation:
            proj["expectation"] = expectation
        self.result.setdefault("aspirations", {}).setdefault("active_projects", []).append(proj)
        return self

    def add_identity_aspiration(self, asp: str):
        self.result.setdefault("aspirations", {}).setdefault("identity_aspirations", []).append(asp)
        return self

    def add_skill_to_learn(self, skill: str):
        self.result.setdefault("aspirations", {}).setdefault("skills_to_learn", []).append(skill)
        return self

    def add_knowledge_gap(self, gap: str):
        self.result.setdefault("aspirations", {}).setdefault("knowledge_gaps", []).append(gap)
        return self

    # ---- Misc ----
    def set_summary(self, summary: str):
        self.result["summary"] = summary
        return self

    def build(self) -> dict:
        return self.result


# ============================================================
# CLI entry
# ============================================================

def main():
    from soul_paths import resolve_soul_dir
    default_soul_dir = str(resolve_soul_dir())
    parser = argparse.ArgumentParser(description="🧬 灵魂提取器")
    parser.add_argument("--soul-dir", default=default_soul_dir,
                        help=f"灵魂数据目录路径（默认: {default_soul_dir}）")
    parser.add_argument("--input", help="对话内容（纯文本，直接传入）")
    parser.add_argument("--mode", default="auto", choices=["auto", "manual", "status"],
                        help="模式")

    args = parser.parse_args()

    archive = SoulArchive(args.soul_dir)

    if args.mode == "status":
        if not archive.is_initialized():
            print("❌ 灵魂存档尚未初始化。请先运行 soul_init.py")
            sys.exit(1)
        print(archive.get_status_report())
        return

    if not args.input:
        print("请通过 --input 提供对话内容（纯文本）")
        sys.exit(1)

    if not archive.is_initialized():
        print("❌ 灵魂存档尚未初始化。请先运行 soul_init.py")
        sys.exit(1)

    print(f"📖 收到对话内容（{len(args.input)} 字符）")
    print(f"📂 灵魂存档路径：{args.soul_dir}")
    print()
    print("请使用 ExtractionBuilder 构建提取结果：")
    print("```python")
    print("from soul_extract import SoulArchive, ExtractionBuilder")
    print(f"archive = SoulArchive('{args.soul_dir}')")
    print("builder = ExtractionBuilder()")
    print("builder.set_identity(name='张三', occupation='程序员')")
    print("builder.add_catchphrase('你懂我意思吧')")
    print("builder.add_topic('人工智能', sentiment='positive', stance='乐观派')")
    print("builder.add_tool('ide', 'VS Code')                 # ⭐ workflow")
    print("builder.add_hard_rule('禁止 git rebase')           # ⭐ workflow")
    print("builder.add_pet_peeve('反感冗长解释')              # ⭐ workflow")
    print("builder.add_long_term_goal('做一个独立开发者')      # ⭐ aspirations")
    print("builder.add_knowledge_gap('Rust 异步编程')          # ⭐ aspirations")
    print("builder.set_summary('本次发现：用户是程序员，重视效率')")
    print("changes = archive.save_extraction(builder.build())")
    print("```")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)