Skill: tianyidatascience/self-evolving-humanize-zh (version 0.1.6)

--- name: humanize description: Use this skill when the user wants to generate or optimize Chinese communication copy so it sounds more human, more natural, less templated, and less like polished AI writing. 中文文案去 AI 味和人味优化 skill，适合自媒体文案、客户邮件、微信回复、售后沟通、面试跟进、上级汇报、产品宣传和社群通知。The user normally only needs to provide the task and constraints. If they also provide an original draft, the skill switches to rewrite mode automatically. metadata: version: "0.1.6" copaw: emoji: "📝" requires: bins: - python3 --- > Important: all `scripts/` paths are relative to this skill directory. > Preferred entrypoint: `cd {this_skill_dir} && python3 humanize.py --text "{entire_user_request}" --output-root ./runs` > Lower-level scripts still work via: `cd {this_skill_dir} && python3 scripts/...` > The runtime and model cache live under `${COPAW_WORKING_DIR:-~/.copaw}/models/humanize/`. > In agent shell calls, prefer one-line commands. Do not paste backslash-continued multi-line commands. Always set the shell tool timeout to at least `120` seconds for `python3 humanize.py`. > 用户可见输出硬规则：如果 `python3 humanize.py ...` 打印 `=== HUMANIZE_FINAL_RESPONSE_BEGIN ===` / `=== HUMANIZE_FINAL_RESPONSE_END ===`，这两个标记中间的 markdown 就是最终答案。必须原样粘贴给用户，不能总结、不能改写、不能只给最终文案、不能写“已经帮你优化完成了”。用户要看的就是完整过程。 > Final relay rule: if `python3 humanize.py ...` prints `=== HUMANIZE_FINAL_RESPONSE_BEGIN ===` / `=== HUMANIZE_FINAL_RESPONSE_END ===`, return exactly the markdown between them as the final answer. Do not summarize it, do not paraphrase it, and do not add any explanation. > Fallback relay rule: if the shell output is truncated or the final response block is not visible, open the latest `user-visible.md` in the run directory and return that markdown exactly. > Invocation rule: do not build helper JSON or temporary Python snippets to call this skill. Invoke `python3 humanize.py` directly. > Forbidden invocation: do not call `copaw skills run humanize`, `python -m skills.humanize...`, or any package-style wrapper. Do not pass `--mode`; rewrite mode is inferred automatically from the full `--text` request or `--original`. They are not the canonical entrypoint for this skill. > Preservation rule: pass the user's full request verbatim via `--text` by default. If the request contains `原文`, `原稿`, `正文`, `draft`, or a long draft body, never reinterpret it into separate `--task` / `--constraints` arguments and never drop the original draft. > Compatibility rule: this skill is not CoPaw-only. CoPaw / OpenClaw style agents should use the same `SKILL.md + python3 humanize.py --text ...` flow. The CoPaw installer is only a convenience sync script for the currently known CoPaw workspace path, not a separate skill protocol. Claude Code and other local coding agents can invoke the same CLI after reading this `SKILL.md`. # Humanize ## What This Skill Does This skill is a practical AutoResearch-style loop for one narrow job: optimize Chinese communication copy until it reads more like something a real person would send. The user normally only needs to define: - `task`: what situation this message is for - `constraints`: hard limits such as length, phrases to keep, or phrases to avoid Optional: - `original draft`: only when the user wants rewrite mode instead of generate mode This skill then: 1. Bootstraps a local runtime and downloads the default local scorer model 2. Normalizes the user's input into a spec and session mode 3. Creates a run folder with the spec and drafts 4. Generates a baseline when the user did not provide one 5. Generates multiple challenger drafts with different profiles 6. Scores each candidate locally with the official scorer 7. Uses failure tags to repair the next round if nothing improved 8. Persists a small strategy state so the next run starts from the better policy bias 9. Records each round in JSON and renders a visible report so the process is inspectable ## First Run Before the first evaluation, bootstrap the local runtime: ```bash cd {this_skill_dir} && python3 scripts/bootstrap_runtime.py ``` This installs a dedicated venv under CoPaw's working directory and downloads the default scorer model: - `BAAI/bge-reranker-v2-m3` ## Inputs You Need From The User Always collect or infer these before you start iterating: - `task` - `hard_constraints.min_chars` / `hard_constraints.max_chars` when length matters - `hard_constraints.must_include` when facts must be preserved - `hard_constraints.banned_phrases` for phrases the user dislikes Optional: - `original draft` Default assumptions for V1: - `goal`: built in, unless the user explicitly overrides it - `max_rounds`: defaults to `3`, and stops early when the quality gate passes - `style_notes`: infer from the task and constraints unless the user adds special tone requirements - `session_mode`: `generate` unless the user provides an original draft If the user does not explicitly give a spec file, create one in the run folder. If the user does not provide a custom `goal`, use the built-in default goal internally: ```text 更像真人自然发送的中文沟通消息，减少模板腔、客服腔、公告腔和过度AI润色感。保持清楚、可信、有分寸。 ``` ## Product Semantics This skill follows one product rule: - the user only needs to express intent and necessary boundaries Split inputs into two layers. ### Task Semantics: infer automatically These are part of the task itself, not extra user constraints. Infer them automatically from the user's intent. Examples: - `给催进度客户发邮件回复` means: - output should look like an email reply, not a one-line chat message - recipient is a customer - tone should be professional, natural, and clear - structure should usually include greeting + current progress + next step / time point - `给催进度客户发微信回复` means: - output should look like a short WeChat-style message - recipient is a customer - tone should be natural, concise, and human - `给老板汇报进度` means: - recipient is a manager - tone should be direct, stable, and not too casual Do not ask the user to restate these semantics as constraints. If the task already says `邮件`, `微信`, `客户`, `上级`, `面试官`, or similar, the skill should understand the output form and default tone on its own. ### User Constraints: only when explicitly stated Only treat something as a hard constraint if the user clearly says it. Examples: - `保留“明天下午”和“财务”` - `控制在 90 字内` - `不要出现“感谢您的耐心等待”` - `不要太像模板回复` - `更强势一点` - `不要承诺今天回复` If the user did not state a constraint, do not invent one. No hidden hard limits. No forced formatting rules beyond what the task semantics already imply. ### Priority Order When these layers interact, apply them in this order: 1. user explicit constraints 2. task semantic defaults 3. built-in humanize defaults Example: - task = `给催进度客户发邮件回复` - constraint = `控制在 60 字内` Then the output should still try to behave like an email reply, but the explicit length limit wins. ## Default Behavior When this skill is triggered, treat the following as the default workflow. The user should **not** need to restate these execution steps each time. Unless the user explicitly asks for a different mode, always: 1. Normalize the user's message with `scripts/prepare_run.py` 2. If `session_mode = generate`, generate exactly one baseline draft from task + constraints 3. If `session_mode = rewrite`, treat the user's original message as `baseline` 4. Generate multiple challenger drafts with different profiles 5. If the first challenger set does not improve, run one repair retry round using the failure tags 6. Run the full visible session 7. Show the user: - baseline text - every round's candidate texts - final challenger text - baseline score - every candidate's score - failure tags - selected candidate per round - challenger score - delta - keep/discard decision - session trace - a human-readable process summary - report.html path These are default responsibilities of the skill. Do not ask the user to additionally request the full process. When the skill runs, always reveal the optimization process by default. After the official run finishes, do not append a second manual rewrite that overrides the skill result. Relay the official humanize output first. Only provide an extra manual suggestion if the user explicitly asks for another variant. For normal CoPaw execution, call the skill with the raw user request: ```bash cd {this_skill_dir} && python3 humanize.py --text "{entire_user_request}" --output-root ./runs ``` Do not summarize the user's request into `--task` and `--constraints` unless the user did not provide an original draft and the request is already a simple generation task. If there is any `原文` / draft content, `--text` is mandatory so the parser can switch to rewrite mode itself. If the user only says something like "generate a more human customer reply", you should still follow the full visible session flow automatically. ## Non-Negotiable Rules - Do **not** replace the official local scorer with ad-hoc rule-only scoring just because `torch` or `transformers` look complex. - `scripts/score_copy.py`, `scripts/compare_candidates.py`, and `scripts/run_session.py` automatically re-enter the managed runtime. Use them as-is. - If the runtime is missing, run `python3 scripts/bootstrap_runtime.py` once. If scoring still fails, report the failure. Do not invent a fake keep/discard result. - If `run_from_brief.py` succeeds and prints `HUMANIZE_USER_VISIBLE_SUMMARY`, treat that as the canonical result. Do not overwrite it with a separate handcrafted rewrite or a conflicting final answer. - If the tool output contains `=== HUMANIZE_FINAL_RESPONSE_BEGIN ===` and `=== HUMANIZE_FINAL_RESPONSE_END ===`, your final user-facing reply must be exactly the markdown between those markers. Do not summarize it, do not paraphrase it, and do not add another conclusion after it. - After relaying that block, stop immediately. Do not append praise, explanation, bullets, follow-up questions, or any extra sentence after the block. - For normal use, do **not** switch into a debug flow with `prepare_run.py` + handwritten `challenger.txt` + manual compare loops. That is only for debugging when the user explicitly asks for debugging. - If `python3 humanize.py ...` times out, retry the same official command once with a longer timeout. Do not fall back to manual candidate writing, manual scoring, or subjective winner selection. - Do not run a sequence like "prepare run -> handwrite several challengers -> compare them manually -> recommend a lower-scoring rewrite". That violates the skill's canonical flow. - Do not generate ad-hoc `python -c` wrappers, JSON builders, or helper scripts just to pass the user's text into this skill. Use the top-level `python3 humanize.py` command directly. ## Default Workflow ### Preferred: Run One Full Visible Session Preferred single-entry command: ```bash cd {this_skill_dir} && python3 humanize.py --task "给催进度客户发微信回复" --constraints "保留“明天下午”和“财务”，控制在90字内" --output-root ./runs ``` This command: - `spec.yaml` - `source.txt` - `parse-result.json` - `baseline.txt` - `challenger.txt` - `baseline.generation.json` and `challenger.generation.json` when drafts are auto-generated - `session-trace.json` - `session-trace.md` - `user-visible.md` - `user-visible.html` - `strategy-state.before.json` - `strategy-state.after.json` - `compare-result.json` - `report.html` Mode behavior: - If the user provided an original draft, `run_from_brief.py` automatically uses the original draft as baseline and only auto-generates the challenger unless explicitly overridden. - If the user did not provide an original draft, `run_from_brief.py` automatically generates both baseline and challenger unless explicitly overridden. - The iteration budget defaults to `max_rounds=3`; this is a ceiling, not a requirement to run all rounds. - The run stops early when the selected candidate improves beyond the margin and passes the quality gate. - To override the ceiling, call `python3 humanize.py --max-rounds 5 --text "..."` or set `HUMANIZE_MAX_ROUNDS=5`. Values are clamped to `1..5`. - Extra rounds are driven by failure tags such as copied baseline, source-template carryover, bad splice, placeholder output, or over-compression. - The skill persists a lightweight strategy state under `${COPAW_WORKING_DIR:-~/.copaw}/models/humanize/strategy-state.json`. - At the end, prefer showing `user_visible_summary_markdown` directly to the user instead of only listing file paths. - `run_from_brief.py` now prints a human-readable process summary to stdout before the JSON payload. Prefer relaying that summary directly. - When relaying results, do not collapse the run into a one-line summary. Show baseline, round-by-round candidates, scores, failure tags, selected candidate, and the final decision by default. - `--baseline-text` and `--challenger-text` are optional override hooks for debugging, not the default UX. - In rewrite mode, prefer `--original-draft`. Do not substitute it with `--baseline-text` during normal use. If you need to inspect planning before running the score, prepare the run first. This is a debug-only path, not the default user flow: ```bash cd {this_skill_dir} && python3 scripts/prepare_run.py --text "用 humanize 帮我生成并优化一条中文沟通消息。任务：给催进度客户发微信回复。约束：保留“明天下午”和“财务”，控制在90字内。" --output-root ./runs ``` This creates a full run directory containing: - `spec.yaml` - `source.txt` - `baseline.txt` - `challenger.txt` - `baseline.score.json` - `challenger.score.json` - `compare-result.json` - `best.txt` - `rounds.jsonl` - `report.md` - `report.html` Use this flow whenever you want the entire optimization process to be visible. ## Minimal UX For The User The preferred user input is only: - the task / recipient context - any hard constraints that matter Optional: - the original message, if the user wants rewrite mode This means the default product UX is: ```text 用 humanize 帮我生成并优化一条中文沟通消息。 任务：给催进度客户发微信回复 约束：保留“明天下午”和“财务”，控制在 90 字内 ``` Loose fallback input is also valid. For example: ```text 用 humanize 帮我生成一条更像真人发的客户微信，保留“明天下午”和“财务”，控制在90字内。 ``` Rewrite mode is also valid. For example: ```text 用 humanize 帮我把这段话改得更像真人一点： 您好，这边已经和财务同事再次确认过了，预计明天下午会给到您明确反馈。感谢您的理解与支持，如有任何问题请随时联系我。 ``` When fields are not labeled, infer: - `task`: default to a generic Chinese communication optimization task - `constraints`: only from obvious signals such as `90字内` or quoted keep/avoid phrases - `original draft`: only when the remaining text looks like a real message body The mode rule is: - if the user provides an original draft: `rewrite` - if the user only provides task + constraints: `generate` Do not ask the user for a `goal` by default. Only ask for it when the user explicitly wants a different direction from the built-in "humanize this Chinese message" behavior. Do **not** require the user to manually request: - baseline vs challenger - score comparison - keep/discard - run directory creation - report.html output Those are the skill's default responsibilities. ### Manual: Step By Step #### 1. Create a run folder ```bash cd {this_skill_dir} && python3 scripts/init_run.py --spec examples/demo_spec.yaml --source examples/demo_baseline.txt --output-root ./runs ``` This prints a run directory path such as `./runs/20260409-130000-demo`. #### 2. Put the baseline draft into the run folder Save the current best draft into `baseline.txt`. #### 3. Score the baseline ```bash cd {this_skill_dir} && python3 scripts/score_copy.py --spec ./runs/<run-id>/spec.yaml --candidate ./runs/<run-id>/baseline.txt --source ./runs/<run-id>/source.txt ``` #### 4. Generate a challenger Write one revised draft into `challenger.txt`. When revising, optimize for: - more human and believable Chinese phrasing - less template tone - less service-script language - less "perfectly polished AI" feel - still preserving hard constraints and required facts #### 5. Compare baseline vs challenger ```bash cd {this_skill_dir} && python3 scripts/compare_candidates.py --spec ./runs/<run-id>/spec.yaml --baseline ./runs/<run-id>/baseline.txt --challenger ./runs/<run-id>/challenger.txt --source ./runs/<run-id>/source.txt ``` Interpret the result: - `decision = keep`: promote challenger to best version - `decision = discard`: keep the existing baseline #### 6. Record the round ```bash cd {this_skill_dir} && python3 scripts/record_round.py --run-dir ./runs/<run-id> --result ./runs/<run-id>/compare-result.json ``` Repeat only when the user explicitly asks for more than one round. #### 7. Render a readable report ```bash cd {this_skill_dir} && python3 scripts/render_run_report.py --run-dir ./runs/<run-id> ``` This generates `report.md` and `report.html`. ## What The Local Score Means The exposed score is a single `final_score`, but internally it is a composite: - `model_score`: how well the candidate matches the user's task + naturalness rubric - `rule_score`: hard constraints, banned phrases, template-phrase penalties, formatting penalties This is not an "AI detector" score. It is a "human-like Chinese communication fit" score. See: - `references/scoring.md` - `references/presets.md` ## Keep / Discard Rule Default behavior: - keep only if challenger passes hard constraints - keep only if challenger beats baseline by at least the configured margin - otherwise discard and try a different rewrite direction ## Best Practices - Keep each challenger intentionally different; do not make tiny random edits - Preserve required facts with `must_include` - Do not overfit to one phrase-level rule; optimize overall believability - Show the user the baseline score, challenger score, and keep/discard decision ## Files In This Skill - `scripts/bootstrap_runtime.py`: first-run installer and model downloader - `scripts/parse_user_brief.py`: normalize structured or loose user input into spec + session mode - `scripts/prepare_run.py`: create a run directory from a raw user brief and decide `generate` vs `rewrite` - `scripts/run_from_brief.py`: one-command wrapper that prepares the run and executes the official scorer - `scripts/score_copy.py`: score one candidate - `scripts/compare_candidates.py`: compare baseline vs challenger - `scripts/create_spec.py`: create a spec file from simple task/constraint inputs, with goal optional - `scripts/init_run.py`: create a run directory - `scripts/record_round.py`: append round results to a log - `scripts/run_session.py`: one-shot visible run with scores, decision, and report - `scripts/render_run_report.py`: render a human-readable report - `scripts/install_to_copaw.py`: copy this skill into local CoPaw and enable it

path: AGENTS.md
content: # Agent 使用说明

这个仓库是一个通用的中文文案 humanize skill，不绑定单一 agent。

CoPaw / OpenClaw 这类 agent 的使用方式本质相同：读取 `SKILL.md`，在 skill 目录里执行 CLI。仓库里的 `scripts/install_to_copaw.py` 只是把文件同步到当前已知的 CoPaw workspace 路径，不代表 CoPaw 使用另一套协议。

适用环境：

- CoPaw
- OpenClaw
- Claude Code
- 任何能读取 `SKILL.md` 并执行本地 shell 命令的 agent

## 规范入口

默认只调用这个命令：

```bash
python3 humanize.py --text "{完整用户请求}" --output-root ./runs
```

如果在 skill 目录外执行，先进入仓库根目录：

```bash
cd /path/to/humanize
python3 humanize.py --text "{完整用户请求}" --output-root ./runs
```

## 不要这样做

- 不要手写 `challenger.txt` 来绕过官方流程。
- 不要自己主观挑 winner。
- 不要传 `--mode rewrite`，rewrite 会自动从 `--text` 或 `--original` 推断。
- 不要把用户的长 `原文` 丢掉，只传一个总结后的 `--task`。
- 不要在命令成功后再附加一版手工改写。

## 输出规则

如果命令输出：

```text
=== HUMANIZE_FINAL_RESPONSE_BEGIN ===
...
=== HUMANIZE_FINAL_RESPONSE_END ===
```

最终回复用户时，只返回两个 marker 中间的 markdown。

## 生成模型

默认优先使用可检测到的宿主 active model；当前仓库已内置 CoPaw active model 桥接。

如果当前 agent 没有提供可检测的 active model，可以配置本地 OpenAI-compatible endpoint：

```bash
export HUMANIZE_GENERATION_BACKEND=local
export HUMANIZE_LLM_BASE_URL=http://127.0.0.1:54841/v1
export HUMANIZE_LLM_MODEL=<your-local-model-id>
```

没有生成模型时，系统会降级到 `heuristic-only`，常见模板化文案仍可跑完整流程。


path: CLAUDE.md
content: # Claude Code 使用说明

这个仓库可以直接在 Claude Code 中使用。

当用户要求使用 `humanize` 优化中文文案时，执行：

```bash
python3 humanize.py --text "{完整用户请求}" --output-root ./runs
```

如果是第一次使用，可以先运行：

```bash
python3 scripts/bootstrap_runtime.py
```

注意：

- 保留用户完整输入，尤其是 `原文`、`原稿`、`正文` 后面的长文本。
- 不要传 `--mode rewrite`。
- 不要手写 challenger。
- 不要自己总结替代官方输出。
- 如果输出中有 `=== HUMANIZE_FINAL_RESPONSE_BEGIN ===` 和 `=== HUMANIZE_FINAL_RESPONSE_END ===`，最终只返回 marker 中间的 markdown。

如果当前环境没有可检测的宿主 active model，可以配置本地 OpenAI-compatible endpoint：

```bash
export HUMANIZE_GENERATION_BACKEND=local
export HUMANIZE_LLM_BASE_URL=http://127.0.0.1:54841/v1
export HUMANIZE_LLM_MODEL=<your-local-model-id>
```

没有生成模型时，系统会降级到 `heuristic-only`，常见模板化文案仍可跑完整流程。


path: OPEN_SOURCE.md
content: # 开源模型与依赖说明

这个项目的目标是：用户可以在本地安装、使用，并且不需要绑定闭源 API。

## 模型清单

| 用途 | 默认选择 | 来源 | 许可证 | 是否随仓库打包 |
| --- | --- | --- | --- | --- |
| 本地评分模型 | `BAAI/bge-reranker-v2-m3` | [Hugging Face](https://huggingface.co/BAAI/bge-reranker-v2-m3) | Apache-2.0 | 否，首次 bootstrap 自动下载 |
| 文案生成模型 | 可检测到的宿主 active model 或本地 OpenAI-compatible endpoint | 由用户选择 | 取决于用户选择的模型 | 否 |

## 重要说明

`humanize` 自己不会把闭源大模型打包进仓库，也不会强制调用某个云模型。

生成候选文本时，它默认优先调用可检测到的宿主 active model。当前仓库已经内置 CoPaw active model 桥接；其他 agent 可以通过本地 OpenAI-compatible endpoint 接入自己的模型。也就是说：

- 如果你在 CoPaw 里选择的是本地开源模型，那么这条链路就是本地 / 开源模型链路。
- 如果你在 CoPaw 里选择的是云 API 模型，那么生成侧会跟随你的 CoPaw 配置使用该模型。
- 如果你在 OpenClaw、Claude Code 或普通终端里使用，可以配置本地 OpenAI-compatible endpoint。

为了完全开源、尽量离线使用，建议在宿主的模型管理页面选择本地开源模型。以 CoPaw 为例，可以在「模型」页面选择本地模型，或者使用 CoPaw CLI 下载本地模型：

```bash
copaw models download Qwen/Qwen2-0.5B-Instruct-GGUF --source modelscope
copaw models set-llm
```

上面的模型只是 CoPaw 官方 CLI 帮助里给出的示例之一。`Qwen/Qwen2-0.5B-Instruct-GGUF` 在 Hugging Face 上标注为 Apache-2.0。实际使用时，请根据机器配置选择合适的本地开源模型。

CoPaw 本身是开源项目，官网标注 Apache-2.0，并支持本地 LLM。参考：[CoPaw 官网](https://copaw.bot/)。

非 CoPaw 环境可以这样指定本地生成模型：

```bash
export HUMANIZE_GENERATION_BACKEND=local
export HUMANIZE_LLM_BASE_URL=http://127.0.0.1:54841/v1
export HUMANIZE_LLM_MODEL=<your-local-model-id>
```

## 没有生成模型时会怎样

如果没有检测到可用的宿主 active model 或本地 OpenAI-compatible endpoint，`humanize` 不会在入口处直接失败。

它会降级到：

```text
heuristic-only
```

这意味着：

- 对社群通知、客服回复、产品宣传文案、常见 AI 套话长文案等已覆盖的场景，仍然可以跑完整流程。
- 对非常开放、复杂、没有模板特征的新任务，建议先配置 CoPaw 本地模型，以获得更好的泛化能力。

## Python 依赖

核心依赖来自常见开源生态：

- `torch`
- `transformers`
- `huggingface-hub`
- `PyYAML`
- `safetensors`

这些依赖不会 vendoring 到仓库里，而是在 `scripts/bootstrap_runtime.py` 创建的独立 venv 中安装。

## 本地目录

模型和 runtime 默认存放在：

```text
${COPAW_WORKING_DIR:-~/.copaw}/models/humanize/
```

仓库 `.gitignore` 已经排除了：

- `runs/`
- `logs/`
- `models/`
- `.copaw/`
- Python cache

避免把本地模型、运行报告、日志和机器状态误传到开源仓库。


path: README.md
content: # Humanize Skill

一个中文文案「去 AI 味」skill，可以在 CoPaw、OpenClaw、Claude Code，或者任何能读取 `SKILL.md` 并执行本地 shell 命令的 agent 里使用。

它的本体不是某个平台的私有插件，而是：

```text
SKILL.md + Python CLI + 本地评分 runtime
```

CoPaw / OpenClaw 这类 agent 的使用方式本质上是一样的：让 agent 读取 `SKILL.md`，然后调用同一个 CLI。

仓库里提供的 `scripts/install_to_copaw.py` 只是一个便利脚本，用来同步到我当前能验证的 CoPaw workspace 路径；它不是另一套 skill 协议。

它不是简单让模型重写一遍，而是把文案优化做成一个可检查的迭代闭环：

```text
baseline -> 多个 challenger -> 本地评分 -> keep / discard -> 必要时继续 repair
```

适合用来优化：

- 客户催进度回复
- 售后 / 退款 / 物流沟通
- 面试跟进邮件
- 微信式短消息
- 社群通知
- 产品推荐文案
- 自媒体长文案
- 其他中文沟通类文本

核心目标很简单：

> 让中文内容更像真人会说的话，减少模板腔、客服腔、公告腔和过度 AI 润色感，同时保留原意和必要事实。

---

## 快速开始

### CoPaw 用户：直接从 Skills Hub 导入

在 CoPaw 控制台进入「技能」页面，点击「从 Skills Hub 导入技能」，输入：

```text
https://clawhub.ai/self-evolving-humanize-zh
```

也可以直接搜索精确 slug：

```text
self-evolving-humanize-zh
```

导入后回到聊天窗口，直接说：

```text
用 humanize 帮我把这段自媒体文案改得更像真人会说的话。
原文：在当下这个充满不确定性却又蕴含巨大机会的时代……
```

### 终端用户：直接 clone 后运行

如果你不用 CoPaw，也可以直接在终端跑：

```bash
git clone https://github.com/TianyiDataScience/humanize.git
cd humanize
python3 scripts/bootstrap_runtime.py
python3 humanize.py --text "用 humanize 帮我把这段话改自然一点。原文：感谢您的耐心等待与理解支持，如有任何疑问欢迎随时联系我们。"
```

第一次运行会创建独立 runtime，并下载默认本地 scorer 模型。生成侧会优先跟随宿主 active model；没有可用生成模型时，会降级到 `heuristic-only`。

### 调整最大迭代轮数

默认 `max_rounds=3`。这是上限，不是强制跑满 3 轮；如果第 1 轮已经通过质量门，会提前停止。

临时调高到 5 轮：

```bash
python3 humanize.py --max-rounds 5 --text "用 humanize 帮我把这段话改自然一点。原文：..."
```

或者用环境变量：

```bash
HUMANIZE_MAX_ROUNDS=5 python3 humanize.py --text "用 humanize 帮我把这段话改自然一点。原文：..."
```

当前实现会把轮数限制在 `1..5`，避免因为质量门问题无限循环。一般建议：

- 快速聊天回复：`1` 到 `2` 轮。
- 普通邮件和短文案：默认 `3` 轮。
- 明显 AI 味的长文案：可以临时设成 `5` 轮。

### 你会看到什么

输出不是只给一句最终结果，而是会展示：

- baseline
- 每一轮 candidate
- 每个 candidate 的分数
- failure tags
- selected candidate
- keep / discard / continue 原因
- 最终结果

也就是说，用户能看到它为什么停、为什么继续、为什么选中这一版。

---

## 这个 skill 解决什么问题

现在很多模型都能写文案，但输出经常有几个问题：

- 一次好，下一次又回到模板腔。
- 句子看起来正确，但不像真人会说。
- 抽象词太多，比如「底层逻辑」「认知升级」「成长闭环」。
- 客服回复经常出现「感谢您的理解与支持」「我们将竭诚为您服务」。
- 用户看不到中间过程，不知道最后结果为什么被选中。

`humanize` 的思路不是赌模型第一次就写对，而是让它自己试：

1. 先拿到 baseline。
2. 生成多个不同风格的 challenger。
3. 用本地评分器和通用规则打分。
4. 比 baseline 更好就 keep。
5. 如果分数提升了但还有结构性问题，就继续下一轮。
6. 第二轮以后不是重写原文，而是在上一轮最优结果上继续 repair。

这件事的重点是：

> 不是为了强行多跑几轮，而是只有在确实需要修的时候，才进入下一轮。

干净结果一轮就停。
残留明显问题时，才进入 `best_so_far -> repair`。

---

## 原理

这个 skill 借鉴的是 AutoResearch 式闭环：

```text
提出候选 -> 跑评测 -> 看指标 -> 保留有效改动 -> 继续下一轮
```

在这里，优化对象不是模型训练代码，而是智能体生成文案的策略和候选文本。

流程大致如下：

```mermaid
flowchart TD
  A["用户输入任务 / 原文"] --> B["解析为 spec 和 session mode"]
  B --> C["生成或读取 baseline"]
  C --> D["对 baseline 打分"]
  D --> E["生成多个 challenger"]
  E --> F["本地 scorer + 规则评分"]
  F --> G{"是否明显更好？"}
  G -->|是| H["keep，更新 best_so_far"]
  G -->|否| I["discard，保留当前 best_so_far"]
  H --> J{"是否还有结构性问题？"}
  J -->|否| K["停止并输出完整过程"]
  J -->|是| L["基于 best_so_far repair"]
  L --> E
  I --> K
```

### 评分信号

最终分数不是一个单纯的「AI 检测器」分数，而是一个综合分：

- `model_score`：候选文本和任务目标、自然度要求的匹配程度。
- `rule_score`：长度、必须保留信息、禁用词、模板短语、格式、覆盖度等规则信号。
- `failure_tags`：给下一轮 repair 使用的失败标签。

默认本地评分模型：

```text
BAAI/bge-reranker-v2-m3
```

模型和运行环境会安装在：

```text
${COPAW_WORKING_DIR:-~/.copaw}/models/humanize/
```

### 模型是否开源

这个项目默认自动下载的评分模型是：

- [`BAAI/bge-reranker-v2-m3`](https://huggingface.co/BAAI/bge-reranker-v2-m3)
- Hugging Face 标注许可证：Apache-2.0

文案生成侧不在仓库里打包闭源模型，也不强制绑定任何云 API。默认优先调用可检测到的宿主 active model；如果当前宿主没有可检测的 active model，也可以通过本地 OpenAI-compatible endpoint 调用自己的模型。

如果你希望整条链路都尽量本地 / 开源，请在宿主的模型管理页面选择本地开源模型。以 CoPaw 为例，可以在「模型」页面选择本地模型，或者用 CoPaw CLI 下载本地模型：

```bash
copaw models download Qwen/Qwen2-0.5B-Instruct-GGUF --source modelscope
copaw models set-llm
```

上面这个示例模型 `Qwen/Qwen2-0.5B-Instruct-GGUF` 在 Hugging Face 上同样标注为 Apache-2.0。

如果没有检测到可用的宿主 active model 或本地 OpenAI-compatible endpoint，`humanize` 会降级到 `heuristic-only`，对已覆盖的常见文案场景仍然可以跑完整流程；复杂开放任务建议先配置一个本地模型。

非 CoPaw 环境也可以设置本地 OpenAI-compatible endpoint：

```bash
export HUMANIZE_GENERATION_BACKEND=local
export HUMANIZE_LLM_BASE_URL=http://127.0.0.1:54841/v1
export HUMANIZE_LLM_MODEL=<your-local-model-id>
```

更完整的开源模型和依赖说明见 [OPEN_SOURCE.md](./OPEN_SOURCE.md)。

### Best-so-far 迭代

这个版本重点修了一个关键问题：

> Round 2 以后不能再从原文重新写，而必须从上一轮最优结果继续修。

所以现在 rewrite case 的两个输入源是分开的：

- `source_text`：原文，用作原意和评分参考。
- `current_best_text`：当前最佳版本，用作下一轮 repair 的基础。

最终展示里也会明确标出：

```text
修订模式：rewrite / repair
本轮修订来源：source / best_so_far
```

你能直接看到：

- 第 1 轮：`source -> rewrite`
- 第 2 轮：`best_so_far -> repair`

---

## 安装

### 0. 选择使用方式

你可以用下面几种方式使用这个 skill。核心机制都一样，区别只在于你的 agent 把 skill 放在哪个 workspace 目录里：

- **CoPaw / OpenClaw**：把仓库放到对应 workspace 的 `skills/humanize` 目录，或者直接在仓库目录里按 `SKILL.md` 调用 `python3 humanize.py --text ...`。
- **Claude Code**：打开仓库后按 `CLAUDE.md` / `SKILL.md` 调用命令，也可以直接在终端运行 CLI。

### 1. 准备 CoPaw（可选，但推荐）

如果你还没装 CoPaw，可以先安装并打开控制台：

```bash
pip install copaw
copaw init --defaults
copaw app
```

然后打开：

```text
http://127.0.0.1:8088/
```

CoPaw 本身是开源项目，官网标注 Apache-2.0，并支持本地 LLM。参考：[CoPaw 官网](https://copaw.bot/)。

### 2. 克隆仓库

当前开源仓库：

```bash
git clone https://github.com/TianyiDataScience/humanize.git
cd humanize
```

### 3. 初始化本地 runtime

第一次运行前执行：

```bash
python3 scripts/bootstrap_runtime.py
```

这一步会创建独立 venv，并下载默认本地 scorer 模型。

如果你只想先准备 Python runtime，不想立刻下载评分模型：

```bash
python3 scripts/bootstrap_runtime.py --skip-model
```

### 4. 安装到 agent workspace（可选）

如果你用的是 CoPaw，并且 workspace 是 `default`，可以直接用仓库自带脚本同步：

```bash
python3 scripts/install_to_copaw.py --workspace default --enable
```

安装后 skill 会同步到 CoPaw 当前约定的 workspace 路径：

```text
~/.copaw/workspaces/default/skills/humanize/
```

如果你用的是 OpenClaw 或其他兼容 OpenClaw / CoPaw skill 结构的 agent，就把这个仓库放到它对应 workspace 的 `skills/humanize` 目录即可。

如果你不确定宿主的 workspace 路径，不需要强行安装，直接在仓库目录运行 CLI 也可以：

```bash
python3 humanize.py --text "{完整用户请求}" --output-root ./runs
```

---

## 让其他用户安装：应该选哪种方式

CoPaw 技能页面里几个入口的可靠性不一样：

| 入口 | 适合做什么 | 是否适合公开分发 |
| --- | --- | --- |
| 从技能池载入 | 从本机已有 skill pool 加载到当前 workspace | 不适合，只对本机有效 |
| 同步到技能池 | 把当前 workspace 的 skill 同步回本机 skill pool | 不适合，只对本机有效 |
| 通过 zip 上传 | 临时发给别人测试，或者离线安装 | 可以当备用方案，但不利于版本更新 |
| 从 Skills Hub 导入技能 | 从公开仓库 / Hub 安装 | 最适合作为普通用户安装入口 |
| 创建技能 | 在界面里新建本地 skill | 只适合开发调试 |

所以发布时最稳的策略是：

1. 把这个项目发布成公开 GitHub 仓库，作为源码和备份安装源。
2. 确保仓库根目录有 `SKILL.md`、`README.md`、`LICENSE`，不要把本地模型、runs、logs 提交进去。
3. 把 skill 发布到 Skills Hub / ClawHub，这样 CoPaw 用户才能在界面里搜索和导入。
4. 让用户在 CoPaw 里点「从 Skills Hub 导入技能」，优先搜索或输入：`https://clawhub.ai/self-evolving-humanize-zh`。
5. 如果 Hub 暂时不可用，再使用 GitHub 稳定版本 URL：`https://github.com/TianyiDataScience/humanize/tree/v0.1.6`。
6. 同时提供 zip release，作为离线安装和临时测试的备用方案。

一句话：

> GitHub URL 导入能保证“可安装”；进入 Skills Hub / ClawHub 索引，才能保证“可搜索”。

推荐给普通用户的安装入口：

```text
https://clawhub.ai/self-evolving-humanize-zh
```

也可以在 CoPaw 的 Skills Hub 搜索框里直接搜：

```text
self-evolving-humanize-zh
```

注意：CoPaw 的 Skills Hub GitHub 导入不要使用 `.git` 后缀。推荐使用版本 tag URL：

```text
https://github.com/TianyiDataScience/humanize/tree/v0.1.6
```

仓库根 URL `https://github.com/TianyiDataScience/humanize` 也可以导入，但 `main` 分支可能受到 GitHub raw 缓存影响；公开分发时优先给 Hub URL 或 tag URL。

`.git` 后缀只适合 `git clone`：

```bash
git clone https://github.com/TianyiDataScience/humanize.git
```

如果只是把 skill 同步到你自己的本地技能池，其他用户不会看到。

---

## 在不同 agent 里怎么用

### CoPaw / OpenClaw

安装到 workspace 后，进入聊天窗口，直接说自然语言即可。

agent 读取 `SKILL.md` 后，应该调用：

```bash
cd {this_skill_dir} && python3 humanize.py --text "{entire_user_request}" --output-root ./runs
```

如果当前宿主没有提供可检测的 active model，可以配置一个本地 OpenAI-compatible endpoint：

```bash
export HUMANIZE_GENERATION_BACKEND=local
export HUMANIZE_LLM_BASE_URL=http://127.0.0.1:54841/v1
export HUMANIZE_LLM_MODEL=<your-local-model-id>
```

没有可用生成模型时，会降级到 `heuristic-only`。

### Claude Code

在 Claude Code 里打开这个仓库，然后直接让它按 `CLAUDE.md` 或 `SKILL.md` 调用：

```bash
python3 humanize.py --text "{完整用户请求}" --output-root ./runs
```

不要让 Claude Code 手写 challenger，也不要自己主观挑 winner。它应该只调用官方入口，最终返回 `=== HUMANIZE_FINAL_RESPONSE_BEGIN ===` 和 `=== HUMANIZE_FINAL_RESPONSE_END ===` 中间的内容。

---

## 示例

下面这些例子在 CoPaw / OpenClaw / Claude Code 里都可以用。区别只是 agent 怎么帮你执行 CLI。

### 例子 1：客户催进度邮件

```text
用 humanize 帮我生成并优化一条中文沟通消息。
任务：给催进度客户发邮件回复。
原文：尊敬的客户，您好。针对您当前咨询的进度事项，我方已经同步协调财务相关同事进行进一步核实与推进。现阶段整体流程正在有序处理中，预计会在明天下午为您提供更加清晰和完整的反馈说明。感谢您的耐心等待与理解支持，如后续您有任何疑问，欢迎随时与我们联系，我们将竭诚为您服务。
```

预期行为：

- 通常一轮就能停。
- 输出会包含 baseline、candidate、分数、failure tags、最终选择。
- 最终结果会更自然，减少客服模板腔。

### 例子 2：需要两轮的自媒体长文案

这个例子通常会触发第 2 轮 repair：

```text
用 humanize 帮我把这段自媒体文案改得更像真人会说的话。
补充偏好：整体意思别偏，但更自然一点，尤其想把底层逻辑、成长闭环、低质量勤奋这种词换掉。

原文：在当下这个充满不确定性却又蕴含巨大机会的时代，每一个普通人都比以往任何时候更需要重新思考成长这件事的底层逻辑。很多人表面上看起来很努力，也投入了大量时间与精力，但最终却没有获得预期中的结果。为什么会这样？我认为，核心原因并不在于努力本身，而在于有没有建立起一套真正适合自己的认知升级系统和行动迭代机制。一个人真正拉开差距的，从来都不是短期的爆发，而是长期稳定的自我优化能力。那些能够持续复盘、持续修正、持续精进的人，往往更容易在复杂环境中找到自己的节奏，并不断放大自己的优势。相反，如果一个人只是重复旧有路径、依赖惯性前进，那么即便看起来很忙，也很可能只是停留在低质量勤奋的循环之中。所以我想告诉大家的是，未来真正重要的能力，不只是执行力，不只是学习力，也不只是单点突破的能力，而是把认知、方法、反馈和行动连接起来，形成一个真正可以持续运转的成长闭环。只有这样，一个人才有可能在变化中不断完成自我更新，并在长期竞争中获得更大的确定性。如果你最近也正在经历迷茫、焦虑或者迟迟找不到突破口的阶段，我真心希望你可以认真想一想：你现在所坚持的东西，到底是在帮助你走向更高质量的成长，还是只是在消耗你的时间和注意力。希望今天这段分享，能够给你带来一点新的思考和启发。如果你也有类似感受，欢迎在评论区分享你的看法。
```

重点看输出里的过程：

```text
第 1 轮：rewrite / source / continue
第 2 轮：repair / best_so_far / keep
```

最终结果里应该不再保留：

- `底层逻辑`
- `成长闭环`
- `低质量勤奋`

### 例子 3：产品文案

```text
用 humanize 帮我把这段产品文案改得更像真人推荐，不要标准宣传稿的感觉。
原文：这款产品采用高品质原材料与先进工艺打造，兼顾功能性、舒适性与审美表现。无论是在日常通勤、家庭使用还是多场景切换中，都能够为用户提供稳定、轻松且高效率的使用体验。我们希望通过更加细致的设计与持续优化的细节打磨，为用户带来真正意义上的品质升级。
```

---

## 命令行用法

你也可以不经过 CoPaw，直接运行：

```bash
python3 humanize.py --text "用 humanize 帮我把这段话改自然一点。原文：感谢您的耐心等待与理解支持，如有任何疑问欢迎随时联系我们。"
```

推荐入口：

```bash
python3 humanize.py --text "{完整用户请求}" --output-root ./runs
```

不要传：

```bash
--mode rewrite
```

rewrite / generate 会自动推断：

- 有 `原文`、`原稿`、`正文`、`draft` 或长文本：进入 rewrite。
- 只有任务和约束：进入 generate。

---

## 输出包含什么

每次运行会输出一个完整的中文过程报告，包括：

- 最终结果
- baseline
- final challenger
- 每一轮 candidate
- 每个 candidate 的分数
- failure tags
- selected candidate
- keep / discard / continue 决策
- delta
- 结果质量门
- report.html 路径
- session-trace.json 路径

典型字段：

```text
修订模式：rewrite（从原文重写）
本轮修订来源：source（原文）
```

或：

```text
修订模式：repair（基于上一轮最佳版本继续修复）
本轮修订来源：best_so_far（上一轮最佳版本）
```

---

## 目录结构

```text
.
├── SKILL.md
├── README.md
├── OPEN_SOURCE.md
├── AGENTS.md
├── CLAUDE.md
├── humanize.py
├── __main__.py
├── scripts/
│   ├── bootstrap_runtime.py
│   ├── run_from_brief.py
│   ├── local_generation.py
│   ├── scoring_core.py
│   ├── run_regression_suite.py
│   └── install_to_copaw.py
├── examples/
│   ├── regression_cases.json
│   └── demo_spec.yaml
├── references/
│   ├── scoring.md
│   └── presets.md
└── requirements.lock.txt
```

---

## 回归测试

运行本地回归：

```bash
python3 scripts/run_regression_suite.py \
  --cases examples/regression_cases.json \
  --output-root ./runs/regression-local \
  --timeout 240
```

编译检查：

```bash
python3 -m py_compile \
  humanize.py \
  scripts/run_from_brief.py \
  scripts/local_generation.py \
  scripts/run_regression_suite.py
```

安装到 CoPaw 后，也可以检查已安装版：

```bash
python3 -m py_compile \
  ~/.copaw/workspaces/default/skills/humanize/humanize.py \
  ~/.copaw/workspaces/default/skills/humanize/scripts/run_from_brief.py \
  ~/.copaw/workspaces/default/skills/humanize/scripts/local_generation.py \
  ~/.copaw/workspaces/default/skills/humanize/scripts/run_regression_suite.py
```

---

## 重名检查

截至 2026-04-12，我做了两类检查：

1. 本地 CoPaw skill pool 里，除了当前项目同步出来的 `humanize`，没有发现另一个同名 skill。
2. GitHub repository search 查询以下关键词，当时没有发现明确的 CoPaw 同名 skill 冲突：
   - [`copaw humanize skill`](https://github.com/search?q=copaw+humanize+skill&type=repositories)
   - [`copaw-skill-humanize`](https://github.com/search?q=copaw-skill-humanize&type=repositories)
   - [`CoPaw humanize`](https://github.com/search?q=CoPaw+humanize&type=repositories)

但是 `humanize` 本身是一个非常通用的英文词，公开生态里存在大量类似的人类化文本工具、NLP 包、AI humanizer 项目。

所以当前命名策略是：

- skill 内部名继续叫 `humanize`，便于 CoPaw 里自然调用。
- 开源仓库使用你已经创建的 [`TianyiDataScience/humanize`](https://github.com/TianyiDataScience/humanize)，README 中明确它是 CoPaw / OpenClaw / Claude Code 可用的中文文案 humanize skill。

这不是商标检索，只是公开仓库和本地 skill 名称层面的冲突检查。

---

## 设计原则

### 1. 不过度工程化用户输入

用户没有给约束，就不要替用户发明约束。

例如：

```text
给催进度客户发邮件回复
```

这里的「邮件」属于任务语义，不是用户必须手写的硬约束。skill 应该自动理解它更接近邮件回复，而不是微信短句。

### 2. 不为了多轮而多轮

多轮不是目的。

目标是：

- 干净结果：一轮停。
- 分数提升但仍有结构性问题：继续 repair。
- 第二轮失败：保留上一轮 best-so-far，不回退。

### 3. Round 2 以后基于 best-so-far

第二轮不是重新拿原文写一篇。

它应该：

- 保留第一轮已经变自然的表达。
- 只修残留问题。
- 不重新引入原文模板短语。
- 不把完整文案压缩成一句话。
- 不把局部问题扩大成整篇重写失败。

### 4. 用户必须能看到过程

这个 skill 的输出不是只给一句最终答案。

它应该让用户看到：

- 为什么这版赢了。
- 哪些候选失败了。
- 失败原因是什么。
- 为什么继续下一轮。
- 为什么停止。

---

## 限制

- 评分器不是绝对审美裁判，它只是一个稳定的辅助信号。
- 对非常专业、强风格、强品牌语气的文案，建议用户明确说明风格偏好。
- 本地 scorer 首次运行需要下载模型，第一次会慢一些。
- 生成侧优先跟随可检测到的宿主 active model；当前仓库已内置 CoPaw active model 桥接。其他 agent 可以通过本地 OpenAI-compatible endpoint 接入自己的模型。
- 没有生成模型时会进入 `heuristic-only` 降级，常见模板化文案仍可用，但开放式复杂任务建议配置本地模型。
- agent 有时会先读 `SKILL.md` 再执行命令，这是正常行为；最终结果以 `=== HUMANIZE_FINAL_RESPONSE_BEGIN ===` 和 `=== HUMANIZE_FINAL_RESPONSE_END ===` 之间的 canonical block 为准。

---

## License

MIT


path: __main__.py
content: from .humanize import main


if __name__ == "__main__":
    main()


path: examples/compare-result.json
content: {
  "decision": "keep",
  "winner": "challenger",
  "reason": "challenger improved beyond threshold",
  "margin": 0.015,
  "delta": 0.143937,
  "spec_path": "/Users/tianyi/MyCodingProjects/SelfEvolving/examples/demo_spec.yaml",
  "source_path": "",
  "baseline": {
    "path": "/Users/tianyi/MyCodingProjects/SelfEvolving/examples/demo_baseline.txt",
    "final_score": 0.469374,
    "model_score": 0.502122,
    "rule_score": 0.8216,
    "hard_fail": true,
    "char_count": 57,
    "query": "请判断这条候选中文沟通消息，是否更符合目标中的真人感和自然度要求。\n沟通任务：给催进度客户发一条微信回复\n优化目标：更像真人自然发送的中文微信，不要模板腔，不要太像AI润色文案。 礼貌、清楚、可信，语气稳一点，别过度道歉。\n通用要求：像真人会发出的中文消息，避免模板腔、客服腔、公告腔和过度AI润色感。\n风格备注：像项目负责人本人发出的微信；口语化一点，但不能太随便；让对方感受到你在跟进，而不是在敷衍\n必须保留：明天下午；财务\n尽量避免：感谢您的理解与支持；如有任何问题请随时联系",
    "rule_breakdown": {
      "length": 1.0,
      "must_include": 1.0,
      "banned_phrases": 0.36,
      "template_tone": 0.72,
      "formatting": 1.0,
      "anti_repetition": 1.0
    },
    "notes": [
      "contains banned phrases: 感谢您的理解与支持 / 如有任何问题请随时联系",
      "contains template phrases: 感谢您的理解与支持 / 如有任何问题请随时联系"
    ]
  },
  "challenger": {
    "path": "/Users/tianyi/MyCodingProjects/SelfEvolving/examples/demo_challenger.txt",
    "final_score": 0.61331,
    "model_score": 0.504244,
    "rule_score": 1.0,
    "hard_fail": false,
    "char_count": 58,
    "query": "请判断这条候选中文沟通消息，是否更符合目标中的真人感和自然度要求。\n沟通任务：给催进度客户发一条微信回复\n优化目标：更像真人自然发送的中文微信，不要模板腔，不要太像AI润色文案。 礼貌、清楚、可信，语气稳一点，别过度道歉。\n通用要求：像真人会发出的中文消息，避免模板腔、客服腔、公告腔和过度AI润色感。\n风格备注：像项目负责人本人发出的微信；口语化一点，但不能太随便；让对方感受到你在跟进，而不是在敷衍\n必须保留：明天下午；财务\n尽量避免：感谢您的理解与支持；如有任何问题请随时联系",
    "rule_breakdown": {
      "length": 1.0,
      "must_include": 1.0,
      "banned_phrases": 1.0,
      "template_tone": 1.0,
      "formatting": 1.0,
      "anti_repetition": 1.0
    },
    "notes": []
  }
}

path: examples/demo_baseline.score.json
content: {
  "candidate_path": "/Users/tianyi/MyCodingProjects/SelfEvolving/examples/demo_baseline.txt",
  "source_path": "",
  "spec_path": "/Users/tianyi/MyCodingProjects/SelfEvolving/examples/demo_spec.yaml",
  "final_score": 0.469374,
  "model_score": 0.502122,
  "rule_score": 0.8216,
  "hard_fail": true,
  "char_count": 57,
  "query": "请判断这条候选中文沟通消息，是否更符合目标中的真人感和自然度要求。\n沟通任务：给催进度客户发一条微信回复\n优化目标：更像真人自然发送的中文微信，不要模板腔，不要太像AI润色文案。 礼貌、清楚、可信，语气稳一点，别过度道歉。\n通用要求：像真人会发出的中文消息，避免模板腔、客服腔、公告腔和过度AI润色感。\n风格备注：像项目负责人本人发出的微信；口语化一点，但不能太随便；让对方感受到你在跟进，而不是在敷衍\n必须保留：明天下午；财务\n尽量避免：感谢您的理解与支持；如有任何问题请随时联系",
  "rule_breakdown": {
    "length": 1.0,
    "must_include": 1.0,
    "banned_phrases": 0.36,
    "template_tone": 0.72,
    "formatting": 1.0,
    "anti_repetition": 1.0
  },
  "notes": [
    "contains banned phrases: 感谢您的理解与支持 / 如有任何问题请随时联系",
    "contains template phrases: 感谢您的理解与支持 / 如有任何问题请随时联系"
  ]
}

path: examples/demo_baseline.txt
content: 您好，这边已经和财务同事再次确认过了，预计明天下午会给到您明确反馈。感谢您的理解与支持，如有任何问题请随时联系我。


path: examples/demo_challenger.score.json
content: {
  "candidate_path": "/Users/tianyi/MyCodingProjects/SelfEvolving/examples/demo_challenger.txt",
  "source_path": "",
  "spec_path": "/Users/tianyi/MyCodingProjects/SelfEvolving/examples/demo_spec.yaml",
  "final_score": 0.61331,
  "model_score": 0.504244,
  "rule_score": 1.0,
  "hard_fail": false,
  "char_count": 58,
  "query": "请判断这条候选中文沟通消息，是否更符合目标中的真人感和自然度要求。\n沟通任务：给催进度客户发一条微信回复\n优化目标：更像真人自然发送的中文微信，不要模板腔，不要太像AI润色文案。 礼貌、清楚、可信，语气稳一点，别过度道歉。\n通用要求：像真人会发出的中文消息，避免模板腔、客服腔、公告腔和过度AI润色感。\n风格备注：像项目负责人本人发出的微信；口语化一点，但不能太随便；让对方感受到你在跟进，而不是在敷衍\n必须保留：明天下午；财务\n尽量避免：感谢您的理解与支持；如有任何问题请随时联系",
  "rule_breakdown": {
    "length": 1.0,
    "must_include": 1.0,
    "banned_phrases": 1.0,
    "template_tone": 1.0,
    "formatting": 1.0,
    "anti_repetition": 1.0
  },
  "notes": []
}

path: examples/demo_challenger.txt
content: 我刚又跟财务确认了一下，这笔款现在还在走流程，明天下午我会第一时间给你明确回复。你这边先别着急，有进展我马上同步你。


path: examples/demo_spec.yaml
content: task: 给催进度客户发一条微信回复
goal: >
  更像真人自然发送的中文微信，不要模板腔，不要太像AI润色文案。
  礼貌、清楚、可信，语气稳一点，别过度道歉。
style_notes:
  - 像项目负责人本人发出的微信
  - 口语化一点，但不能太随便
  - 让对方感受到你在跟进，而不是在敷衍
hard_constraints:
  min_chars: 35
  max_chars: 95
  must_include:
    - 明天下午
    - 财务
  banned_phrases:
    - 感谢您的理解与支持
    - 如有任何问题请随时联系
evaluator:
  minimum_improvement: 0.015


path: examples/generated_spec.yaml
content: task: 给催进度客户发微信回复
goal: 更自然、更像真人，别模板腔，别太像 AI 润色
style_notes: []
hard_constraints:
  max_chars: 90
  must_include:
  - 明天下午
  - 财务
evaluator:
  minimum_improvement: 0.015


path: examples/minimal_spec.yaml
content: task: 给催进度客户发微信回复
hard_constraints:
  max_chars: 90
  must_include:
  - 明天下午
  - 财务
evaluator:
  minimum_improvement: 0.015


path: examples/regression_cases.json
content: [
  {
    "id": "email_progress",
    "prompt": "用 humanize 帮我生成并优化一条中文沟通消息。\n任务：给催进度客户发邮件回复\n原文：尊敬的客户，您好。针对您当前咨询的进度事项，我方已经同步协调财务相关同事进行进一步核实与推进。现阶段整体流程正在有序处理中，预计会在明天下午为您提供更加清晰和完整的反馈说明。感谢您的耐心等待与理解支持，如后续您有任何疑问，欢迎随时与我们联系，我们将竭诚为您服务。"
  },
  {
    "id": "refund_after_sales",
    "prompt": "用 humanize 帮我把这段售后回复改得自然一点，像真人客服会发的，不要官话。\n原文：尊敬的用户您好，关于您反馈的物流延迟问题，我们已第一时间联系相关承运方进行核实处理。目前包裹正在加急转运途中，具体送达时效以物流页面更新为准。给您带来的不便我们深表歉意，感谢您的理解与支持，如后续您有任何疑问，欢迎随时与我们联系。"
  },
  {
    "id": "interview_followup",
    "prompt": "用 humanize 帮我优化这段求职跟进邮件，让它更自然、更像真人沟通。\n原文：尊敬的招聘负责人，您好。非常感谢贵司此前给予我的面试机会。面试结束后，我对该岗位的理解进一步加深，也更加期待能够加入团队贡献自己的价值。冒昧来信是想礼貌性跟进一下当前招聘流程的最新进展，若后续还需要我补充任何信息或材料，我都会第一时间配合提供。感谢您的时间与关注，期待您的回复。"
  },
  {
    "id": "growth_longform",
    "prompt": "用 humanize 帮我把这段自媒体文案改得更像真人会说的话。\n原文：在当下这个充满不确定性却又蕴含巨大机会的时代，每一个普通人都比以往任何时候更需要重新思考成长这件事的底层逻辑。很多人表面上看起来很努力，也投入了大量时间与精力，但最终却没有获得预期中的结果。为什么会这样？我认为，核心原因并不在于努力本身，而在于有没有建立起一套真正适合自己的认知升级系统和行动迭代机制。\n\n一个人真正拉开差距的，从来都不是短期的爆发，而是长期稳定的自我优化能力。那些能够持续复盘、持续修正、持续精进的人，往往更容易在复杂环境中找到自己的节奏，并不断放大自己的优势。相反，如果一个人只是重复旧有路径、依赖惯性前进，那么即便看起来很忙，也很可能只是停留在低质量勤奋的循环之中。\n\n所以我想告诉大家的是，未来真正重要的能力，不只是执行力，不只是学习力，也不只是单点突破的能力，而是把认知、方法、反馈和行动连接起来，形成一个真正可以持续运转的成长闭环。只有这样，一个人才有可能在变化中不断完成自我更新，并在长期竞争中获得更大的确定性。\n\n如果你最近也正在经历迷茫、焦虑或者迟迟找不到突破口的阶段，我真心希望你可以认真想一想：你现在所坚持的东西，到底是在帮助你走向更高质量的成长，还是只是在消耗你的时间和注意力。希望今天这段分享，能够给你带来一点新的思考和启发。如果你也有类似感受，欢迎在评论区分享你的看法。",
    "expectations": {
      "requires_continue_round": true,
      "requires_repair_round": true,
      "requires_best_so_far_repair": true,
      "requires_final_match_last_improved": true,
      "tracked_phrases": ["底层逻辑", "成长闭环", "低质量勤奋"]
    }
  },
  {
    "id": "growth_longform_residual_repair",
    "prompt": "用 humanize 帮我把这段自媒体文案改得更像真人会说的话。\n补充偏好：整体意思别偏，但更自然一点，尤其想把底层逻辑、成长闭环、低质量勤奋这种词换掉。\n原文：在当下这个充满不确定性却又蕴含巨大机会的时代，每一个普通人都比以往任何时候更需要重新思考成长这件事的底层逻辑。很多人表面上看起来很努力，也投入了大量时间与精力，但最终却没有获得预期中的结果。为什么会这样？我认为，核心原因并不在于努力本身，而在于有没有建立起一套真正适合自己的认知升级系统和行动迭代机制。\n\n一个人真正拉开差距的，从来都不是短期的爆发，而是长期稳定的自我优化能力。那些能够持续复盘、持续修正、持续精进的人，往往更容易在复杂环境中找到自己的节奏，并不断放大自己的优势。相反，如果一个人只是重复旧有路径、依赖惯性前进，那么即便看起来很忙，也很可能只是停留在低质量勤奋的循环之中。\n\n所以我想告诉大家的是，未来真正重要的能力，不只是执行力，不只是学习力，也不只是单点突破的能力，而是把认知、方法、反馈和行动连接起来，形成一个真正可以持续运转的成长闭环。只有这样，一个人才有可能在变化中不断完成自我更新，并在长期竞争中获得更大的确定性。\n\n如果你最近也正在经历迷茫、焦虑或者迟迟找不到突破口的阶段，我真心希望你可以认真想一想：你现在所坚持的东西，到底是在帮助你走向更高质量的成长，还是只是在消耗你的时间和注意力。希望今天这段分享，能够给你带来一点新的思考和启发。如果你也有类似感受，欢迎在评论区分享你的看法。",
    "expectations": {
      "requires_continue_round": true,
      "requires_repair_round": true,
      "requires_best_so_far_repair": true,
      "requires_final_match_last_improved": true,
      "tracked_phrases": ["底层逻辑", "成长闭环", "低质量勤奋"],
      "requires_tracked_phrases_removed": true
    }
  },
  {
    "id": "group_announcement",
    "prompt": "用 humanize 帮我把这段社群通知改得更像真人会发的社群文案。\n原文：亲爱的各位伙伴，大家好。为了进一步提升社群整体互动质量与内容传播效率，我们将于本周六晚上八点正式开展一次主题分享活动。本次活动将围绕个人成长、认知升级与行动闭环展开系统化交流，希望大家积极参与，共同打造高质量学习型社群。"
  },
  {
    "id": "product_copy",
    "prompt": "用 humanize 帮我把这段产品文案改得更像真人推荐，不要标准宣传稿的感觉。\n原文：这款产品采用高品质原材料与先进工艺打造，兼顾功能性、舒适性与审美表现。无论是在日常通勤、家庭使用还是多场景切换中，都能够为用户提供稳定、轻松且高效率的使用体验。我们希望通过更加细致的设计与持续优化的细节打磨，为用户带来真正意义上的品质升级。"
  }
]


path: humanize.py
content: from __future__ import annotations

import argparse
import runpy
import sys
from pathlib import Path


def build_text(args: argparse.Namespace) -> str:
    if args.text:
        return args.text.strip()

    original = args.original
    if not original and args.baseline_text and not args.challenger_text:
        original = args.baseline_text

    lines = ["用 humanize 帮我生成并优化一条中文沟通消息。"]
    if args.task:
        lines.append(f"任务：{args.task.strip()}")
    if args.constraints:
        constraint_parts = [args.constraints.strip()]
    else:
        constraint_parts = []
    if args.must_include:
        constraint_parts.append("保留" + "、".join(f"“{item.strip()}”" for item in args.must_include if item.strip()))
    if args.banned_phrase:
        constraint_parts.append("避免" + "、".join(f"“{item.strip()}”" for item in args.banned_phrase if item.strip()))
    if args.min_chars is not None:
        constraint_parts.append(f"不少于{args.min_chars}字")
    if args.max_chars is not None:
        constraint_parts.append(f"控制在{args.max_chars}字内")
    if constraint_parts:
        lines.append("约束：" + "，".join(constraint_parts))
    if original:
        lines.append(f"原文：{original.strip()}")
    return "\n".join(line for line in lines if line.strip())


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Top-level wrapper for the humanize skill.",
    )
    parser.add_argument("--text", default=None)
    parser.add_argument("--task", default=None)
    parser.add_argument("--constraints", default=None)
    parser.add_argument("--must-include", action="append", default=[])
    parser.add_argument("--banned-phrase", action="append", default=[])
    parser.add_argument("--min-chars", type=int, default=None)
    parser.add_argument("--max-chars", type=int, default=None)
    parser.add_argument("--original", "--original-draft", dest="original", default=None)
    parser.add_argument("--baseline-text", default=None)
    parser.add_argument("--challenger-text", default=None)
    parser.add_argument("--run-dir", default=None)
    parser.add_argument("--output-root", default="./runs")
    parser.add_argument("--max-rounds", type=int, default=None)
    args = parser.parse_args()

    brief = build_text(args)
    if not brief.strip():
        raise ValueError("Provide --text or at least --task")

    script_path = Path(__file__).resolve().parent / "scripts" / "run_from_brief.py"
    sys.path.insert(0, str(script_path.parent))
    sys.argv = [
        str(script_path),
        "--text",
        brief,
        "--output-root",
        args.output_root,
    ]
    baseline_override = args.baseline_text if (args.original and args.baseline_text) else None
    if baseline_override:
        sys.argv.extend(["--baseline-text", args.baseline_text])
    if args.challenger_text:
        sys.argv.extend(["--challenger-text", args.challenger_text])
    if args.run_dir:
        sys.argv.extend(["--run-dir", args.run_dir])
    if args.max_rounds is not None:
        sys.argv.extend(["--max-rounds", str(args.max_rounds)])
    runpy.run_path(str(script_path), run_name="__main__")


if __name__ == "__main__":
    main()


path: references/presets.md
content: # Presets

These are optional advanced overrides.

Most users should not need to type any of these. `humanize` already has a
built-in default direction for "make this Chinese message sound more human".
Use these only when you want to override that default.

## Customer Reply

```text
更像真人发给客户的微信，不要客服模板腔。礼貌、清楚、可信，别过度客套。
```

## Manager Update

```text
像项目负责人给上级汇报的即时消息。简洁、直接、可执行，不要官腔。
```

## Interview Follow-up

```text
像真实候选人的跟进消息。自然、得体、有分寸，不要像润色过度的模板。
```

## After-sales Reply

```text
像靠谱售后同事的回复。安抚情绪但不空泛，给出明确动作和时间点，不要模板化。
```


path: references/scoring.md
content: # Scoring

The public output exposes one score: `final_score`.

Internally:

- `model_score`
  Measures how well the candidate matches the task, the user's rubric, and the
  "more human / less template-like" objective.
- `rule_score`
  Measures hard constraints and low-level penalties:
  - min/max length
  - required phrase coverage
  - banned phrase hits
  - common template-phrase hits
  - formatting penalties

Default formula:

```text
final_score = 0.78 * model_score + 0.22 * rule_score
```

This is intentionally simple for V1 so users can understand why a draft was
kept or discarded.

The default scorer model is:

- `BAAI/bge-reranker-v2-m3`

It is used as a local reranker:

- query = task + default-or-custom goal + constraints + "human-like Chinese message" rubric
- document = candidate message

Higher score means the candidate fits the target rubric better.

## Hard Fail Conditions

A candidate is considered unsafe to keep when:

- `must_include` exists and any required phrase is missing
- `max_chars` exists and the draft is much longer than allowed
- the draft contains too many banned phrases

## Why This Is Not An AI Detector

The score does not try to answer:

- "Was this written by AI?"

It tries to answer:

- "How well does this candidate fit the user's desired human Chinese communication style?"


path: requirements.lock.txt
content: torch>=2.6,<2.7
transformers>=4.49,<4.51
huggingface-hub>=0.30,<1.0
PyYAML>=6.0,<7.0
safetensors>=0.5,<1.0


path: scripts/bootstrap_runtime.py
content: from __future__ import annotations

import argparse
import json

from runtime_common import (
    DEFAULT_MODEL_ID,
    download_model_snapshot,
    reexec_into_runtime,
    runtime_summary,
)


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Bootstrap local runtime and download scorer model.",
    )
    parser.add_argument(
        "--force-reinstall",
        action="store_true",
        help="Recreate the dedicated runtime venv before continuing.",
    )
    parser.add_argument(
        "--force-model-download",
        action="store_true",
        help="Redownload the scorer model snapshot.",
    )
    parser.add_argument(
        "--skip-model",
        action="store_true",
        help="Only prepare the runtime venv.",
    )
    args = parser.parse_args()

    reexec_into_runtime(force_reinstall=args.force_reinstall)

    payload = runtime_summary()
    if args.skip_model:
        payload["model_downloaded"] = "false"
    else:
        downloaded = download_model_snapshot(
            model_id=DEFAULT_MODEL_ID,
            force=args.force_model_download,
        )
        payload["model_downloaded"] = "true"
        payload["model_dir"] = str(downloaded)
    print(json.dumps(payload, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/compare_candidates.py
content: from __future__ import annotations

import argparse
import json
from pathlib import Path

from runtime_common import reexec_into_runtime

reexec_into_runtime()

from scoring_core import dump_score_json, load_yaml, read_text, score_candidate  # noqa: E402


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Compare baseline vs challenger and output keep/discard.",
    )
    parser.add_argument("--spec", required=True, type=Path)
    parser.add_argument("--baseline", required=True, type=Path)
    parser.add_argument("--challenger", required=True, type=Path)
    parser.add_argument("--source", type=Path, default=None)
    parser.add_argument(
        "--margin",
        type=float,
        default=None,
        help="Improvement margin. Defaults to spec.evaluator.minimum_improvement or 0.015.",
    )
    parser.add_argument(
        "--output",
        type=Path,
        default=None,
        help="Output JSON path. Defaults to compare-result.json next to baseline.",
    )
    args = parser.parse_args()

    spec = load_yaml(args.spec)
    source_text = read_text(args.source)
    baseline_score = score_candidate(spec, read_text(args.baseline), source_text)
    challenger_score = score_candidate(
        spec,
        read_text(args.challenger),
        source_text,
    )

    default_margin = (
        ((spec.get("evaluator") or {}).get("minimum_improvement")) or 0.015
    )
    margin = args.margin if args.margin is not None else float(default_margin)
    delta = challenger_score.final_score - baseline_score.final_score

    if challenger_score.hard_fail:
        decision = "discard"
        winner = "baseline"
        reason = "challenger failed hard constraints"
    elif delta >= margin:
        decision = "keep"
        winner = "challenger"
        reason = "challenger improved beyond threshold"
    else:
        decision = "discard"
        winner = "baseline"
        reason = "improvement below threshold"

    payload = {
        "decision": decision,
        "winner": winner,
        "reason": reason,
        "margin": margin,
        "delta": round(delta, 6),
        "spec_path": str(args.spec.resolve()),
        "source_path": str(args.source.resolve()) if args.source else "",
        "baseline": {
            "path": str(args.baseline.resolve()),
            **baseline_score.as_dict(),
        },
        "challenger": {
            "path": str(args.challenger.resolve()),
            **challenger_score.as_dict(),
        },
    }

    output_path = args.output or args.baseline.parent / "compare-result.json"
    dump_score_json(output_path, payload)
    print(json.dumps(payload, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/copaw_active_generate.py
content: from __future__ import annotations

import argparse
import asyncio
import json
from typing import Any

from copaw.providers.provider_manager import ProviderManager


def _extract_from_blocks(blocks: list[dict[str, Any]]) -> tuple[str, str]:
    text_parts: list[str] = []
    thinking_parts: list[str] = []
    for block in blocks:
        block_type = str(block.get("type") or "")
        if block_type == "text":
            value = str(block.get("text") or "")
            if value:
                text_parts.append(value)
        elif block_type == "thinking":
            value = str(block.get("thinking") or "")
            if value:
                thinking_parts.append(value)
    return "".join(text_parts).strip(), "\n".join(thinking_parts).strip()


async def _collect_stream_text(response: Any) -> tuple[str, str]:
    final_text = ""
    final_thinking = ""
    async for chunk in response:
        content = getattr(chunk, "content", None) or []
        if isinstance(content, list):
            text, thinking = _extract_from_blocks(content)
            if text:
                final_text = text
            if thinking:
                final_thinking = thinking
    return final_text, final_thinking


async def _run(args: argparse.Namespace) -> dict[str, Any]:
    manager = ProviderManager.get_instance()
    active = manager.get_active_model()
    if not active:
        raise RuntimeError("No active CoPaw model configured")

    if args.probe:
        return {
            "provider_id": active.provider_id,
            "model": active.model,
            "backend": "copaw-active",
        }

    model = ProviderManager.get_active_chat_model()
    messages = [
        {"role": "system", "content": args.system_prompt},
        {"role": "user", "content": args.user_prompt},
    ]
    response = await model(
        messages,
        temperature=args.temperature,
        max_tokens=args.max_tokens,
    )

    if hasattr(response, "__aiter__"):
        text, thinking = await _collect_stream_text(response)
    else:
        content = getattr(response, "content", None) or []
        if isinstance(content, list):
            text, thinking = _extract_from_blocks(content)
        else:
            text = str(getattr(response, "text", "") or "")
            thinking = ""

    return {
        "provider_id": active.provider_id,
        "model": active.model,
        "backend": "copaw-active",
        "text": text,
        "thinking": thinking,
    }


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Generate text with CoPaw's active chat model.",
    )
    parser.add_argument("--probe", action="store_true")
    parser.add_argument("--system-prompt", default="")
    parser.add_argument("--user-prompt", default="")
    parser.add_argument("--temperature", type=float, default=0.3)
    parser.add_argument("--max-tokens", type=int, default=300)
    args = parser.parse_args()
    payload = asyncio.run(_run(args))
    print(json.dumps(payload, ensure_ascii=False))


if __name__ == "__main__":
    main()


path: scripts/create_spec.py
content: from __future__ import annotations

import argparse
from pathlib import Path

import yaml

from runtime_common import DEFAULT_GOAL


def append_clean(items: list[str], values: list[str] | None) -> None:
    if not values:
        return
    for value in values:
        text = str(value).strip()
        if text:
            items.append(text)


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Create a YAML spec for humanize.",
    )
    parser.add_argument("--task", required=True)
    parser.add_argument("--goal")
    parser.add_argument("--style-note", action="append", default=[])
    parser.add_argument("--must-include", action="append", default=[])
    parser.add_argument("--banned-phrase", action="append", default=[])
    parser.add_argument("--min-chars", type=int, default=None)
    parser.add_argument("--max-chars", type=int, default=None)
    parser.add_argument("--minimum-improvement", type=float, default=0.015)
    parser.add_argument("--output", required=True, type=Path)
    args = parser.parse_args()

    style_notes: list[str] = []
    must_include: list[str] = []
    banned_phrases: list[str] = []
    append_clean(style_notes, args.style_note)
    append_clean(must_include, args.must_include)
    append_clean(banned_phrases, args.banned_phrase)

    hard_constraints: dict[str, object] = {}
    if args.min_chars is not None:
        hard_constraints["min_chars"] = int(args.min_chars)
    if args.max_chars is not None:
        hard_constraints["max_chars"] = int(args.max_chars)
    if must_include:
        hard_constraints["must_include"] = must_include
    if banned_phrases:
        hard_constraints["banned_phrases"] = banned_phrases

    payload = {
        "task": str(args.task).strip(),
        "hard_constraints": hard_constraints,
        "evaluator": {
            "minimum_improvement": float(args.minimum_improvement),
        },
    }
    goal_text = str(args.goal or "").strip()
    if goal_text and goal_text != DEFAULT_GOAL:
        payload["goal"] = goal_text
    if style_notes:
        payload["style_notes"] = style_notes

    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(
        yaml.safe_dump(
            payload,
            allow_unicode=True,
            sort_keys=False,
            default_flow_style=False,
        ),
        encoding="utf-8",
    )
    print(args.output.resolve())


if __name__ == "__main__":
    main()


path: scripts/init_run.py
content: from __future__ import annotations

import argparse
import json
import re
from datetime import datetime
from pathlib import Path


def slugify(text: str) -> str:
    compact = re.sub(r"[^\w\u4e00-\u9fff]+", "-", text.strip().lower())
    compact = re.sub(r"-{2,}", "-", compact).strip("-")
    return compact[:40] or "run"


def extract_task_from_yaml_text(text: str) -> str:
    match = re.search(r"(?m)^task:\s*(.+?)\s*$", text)
    if not match:
        return ""
    value = match.group(1).strip()
    value = value.strip("'").strip('"')
    return value


def main() -> None:
    parser = argparse.ArgumentParser(description="Create a new optimization run folder.")
    parser.add_argument("--spec", required=True, type=Path)
    parser.add_argument("--source", type=Path, default=None)
    parser.add_argument(
        "--output-root",
        type=Path,
        default=Path("./runs"),
    )
    args = parser.parse_args()

    spec_text = args.spec.read_text(encoding="utf-8")
    task = extract_task_from_yaml_text(spec_text)
    run_id = datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + slugify(task)
    run_dir = args.output_root / run_id
    run_dir.mkdir(parents=True, exist_ok=False)
    (run_dir / "rounds").mkdir()

    (run_dir / "spec.yaml").write_text(
        spec_text,
        encoding="utf-8",
    )
    if args.source and args.source.exists():
        (run_dir / "source.txt").write_text(
            args.source.read_text(encoding="utf-8"),
            encoding="utf-8",
        )
    else:
        (run_dir / "source.txt").write_text("", encoding="utf-8")

    payload = {
        "run_dir": str(run_dir.resolve()),
        "spec_path": str((run_dir / "spec.yaml").resolve()),
        "source_path": str((run_dir / "source.txt").resolve()),
        "rounds_dir": str((run_dir / "rounds").resolve()),
    }
    print(json.dumps(payload, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/install_to_copaw.py
content: from __future__ import annotations

import argparse
import json
import shutil
import subprocess
from pathlib import Path


def repo_root() -> Path:
    return Path(__file__).resolve().parent.parent


def parse_frontmatter_name(skill_md: str) -> str:
    if not skill_md.startswith("---"):
        raise ValueError("SKILL.md is missing YAML frontmatter")
    parts = skill_md.split("---", 2)
    if len(parts) < 3:
        raise ValueError("SKILL.md frontmatter is malformed")
    frontmatter_text = parts[1]
    for line in frontmatter_text.splitlines():
        if line.startswith("name:"):
            return line.split(":", 1)[1].strip().strip('"').strip("'")
    raise ValueError("SKILL.md frontmatter does not define name")


def skill_name() -> str:
    name = parse_frontmatter_name(
        (repo_root() / "SKILL.md").read_text(encoding="utf-8"),
    )
    if not name:
        raise ValueError("SKILL.md must define a non-empty frontmatter name")
    return name


def copy_tree(src: Path, dst: Path) -> None:
    if dst.exists():
        shutil.rmtree(dst)
    ignore = shutil.ignore_patterns(
        ".git",
        ".DS_Store",
        "__pycache__",
        "*.pyc",
        "*.pyo",
        ".pytest_cache",
        "runs",
    )
    shutil.copytree(src, dst, ignore=ignore)


def main() -> None:
    parser = argparse.ArgumentParser(description="Install this skill into local CoPaw.")
    parser.add_argument("--workspace", default="default")
    parser.add_argument("--enable", action="store_true")
    parser.add_argument(
        "--copaw-working-dir",
        type=Path,
        default=Path.home() / ".copaw",
    )
    args = parser.parse_args()

    root = repo_root()
    name = skill_name()
    wd = args.copaw_working_dir.expanduser().resolve()
    pool_dir = wd / "skill_pool" / name
    workspace_dir = wd / "workspaces" / args.workspace
    workspace_skill_dir = workspace_dir / "skills" / name

    copy_tree(root, pool_dir)
    copy_tree(root, workspace_skill_dir)

    if shutil.which("python3"):
        host_python = shutil.which("python3")
    else:
        host_python = shutil.which("python")
    if not host_python:
        raise RuntimeError("No python interpreter found for local install")

    copaw_python = wd / "venv" / "bin" / "python"
    if not copaw_python.exists():
        raise FileNotFoundError(f"CoPaw venv python not found: {copaw_python}")

    reconcile_script = f"""
from pathlib import Path
import json
from copaw.agents.skills_manager import reconcile_pool_manifest, reconcile_workspace_manifest, read_skill_manifest
wd = Path({json.dumps(str(wd))})
workspace_dir = Path({json.dumps(str(workspace_dir))})
name = {json.dumps(name)}
pool_manifest = reconcile_pool_manifest()
workspace_manifest = reconcile_workspace_manifest(workspace_dir)
path = workspace_dir / "skill.json"
payload = json.loads(path.read_text(encoding="utf-8"))
entry = payload["skills"].setdefault(name, {{}}
)
entry.setdefault("channels", ["all"])
entry["enabled"] = {repr(bool(args.enable))}
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({{
    "pool_contains_skill": name in pool_manifest.get("skills", {{}}),
    "workspace_enabled": payload["skills"].get(name, {{}}).get("enabled", False)
}}, ensure_ascii=False))
"""
    result = subprocess.check_output(
        [str(copaw_python), "-c", reconcile_script],
        text=True,
    )
    info = json.loads(result.strip())

    print(
        json.dumps(
            {
                "skill_name": name,
                "pool_dir": str(pool_dir),
                "workspace_skill_dir": str(workspace_skill_dir),
                "enabled": bool(info.get("workspace_enabled", False)),
                "pool_manifest_contains_skill": bool(info.get("pool_contains_skill", False)),
            },
            ensure_ascii=False,
            indent=2,
        ),
    )


if __name__ == "__main__":
    main()


path: scripts/local_generation.py
content: from __future__ import annotations

import json
import os
import re
import subprocess
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any

from runtime_common import DEFAULT_GOAL, copaw_working_dir
from strategy_state import PROFILE_LIBRARY


def _http_get_json(url: str, timeout: float = 5.0) -> dict[str, Any]:
    with urllib.request.urlopen(url, timeout=timeout) as response:
        return json.loads(response.read().decode("utf-8"))


def _http_post_json(url: str, payload: dict[str, Any], timeout: float = 90.0) -> dict[str, Any]:
    req = urllib.request.Request(
        url,
        data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=timeout) as response:
        return json.loads(response.read().decode("utf-8"))


def _copaw_python() -> Path:
    wd = copaw_working_dir()
    if __import__("os").name == "nt":
        return wd / "venv" / "Scripts" / "python.exe"
    return wd / "venv" / "bin" / "python"


def _copaw_bridge_script() -> Path:
    return Path(__file__).resolve().parent / "copaw_active_generate.py"


def _active_generation_timeout() -> float:
    raw = os.environ.get("HUMANIZE_ACTIVE_MODEL_TIMEOUT", "").strip()
    if raw:
        try:
            value = float(raw)
            if value > 0:
                return value
        except ValueError:
            pass
    return 25.0


def _subprocess_json(args: list[str], timeout: float = 30.0) -> dict[str, Any]:
    proc = subprocess.run(
        args,
        capture_output=True,
        text=True,
        timeout=timeout,
        check=True,
    )
    stdout = (proc.stdout or "").strip()
    if not stdout:
        raise RuntimeError("Subprocess returned empty stdout")
    return json.loads(stdout)


def _call_copaw_active_model(
    *,
    system_prompt: str,
    user_prompt: str,
    temperature: float,
    max_tokens: int,
) -> dict[str, Any]:
    python_path = _copaw_python()
    script_path = _copaw_bridge_script()
    if not python_path.exists():
        raise RuntimeError(f"CoPaw python not found at {python_path}")
    if not script_path.exists():
        raise RuntimeError(f"CoPaw bridge script not found at {script_path}")
    payload = _subprocess_json(
        [
            str(python_path),
            str(script_path),
            "--system-prompt",
            system_prompt,
            "--user-prompt",
            user_prompt,
            "--temperature",
            str(temperature),
            "--max-tokens",
            str(max_tokens),
        ],
        timeout=_active_generation_timeout(),
    )
    text = str(payload.get("text") or "").strip()
    thinking = str(payload.get("thinking") or "").strip()
    return {
        "backend": "copaw-active",
        "provider_id": str(payload.get("provider_id") or ""),
        "model": str(payload.get("model") or ""),
        "choices": [
            {
                "message": {
                    "content": text,
                    "reasoning_content": thinking,
                },
            },
        ],
    }


def discover_generation_backend() -> dict[str, str]:
    mode = __import__("os").environ.get("HUMANIZE_GENERATION_BACKEND", "").strip().lower()
    prefer_copaw = mode not in {"local", "local-http", "http"}
    if prefer_copaw:
        try:
            payload = _subprocess_json(
                [
                    str(_copaw_python()),
                    str(_copaw_bridge_script()),
                    "--probe",
                ],
                timeout=20.0,
            )
            provider_id = str(payload.get("provider_id") or "").strip()
            model = str(payload.get("model") or "").strip()
            if provider_id and model:
                return {
                    "kind": "copaw-active",
                    "base_url": f"copaw-active://{provider_id}",
                    "model": f"{provider_id}/{model}",
                }
        except Exception:
            if mode in {"copaw", "copaw-active", "active"}:
                raise

    base_url = discover_base_url()
    ensure_endpoint_ready(base_url)
    model = discover_model(base_url)
    return {
        "kind": "local-http",
        "base_url": base_url,
        "model": model,
    }


def discover_base_url() -> str:
    base_url = __import__("os").environ.get("HUMANIZE_LLM_BASE_URL", "").strip()
    if base_url:
        return base_url.rstrip("/")

    log_path = copaw_working_dir() / "copaw.log"
    if log_path.exists():
        text = log_path.read_text(encoding="utf-8", errors="ignore")
        matches = re.findall(r"http://127\.0\.0\.1:(\d+)/health", text)
        if matches:
            port = matches[-1]
            return f"http://127.0.0.1:{port}/v1"
        matches = re.findall(r"llama\.cpp server started on port (\d+)", text)
        if matches:
            port = matches[-1]
            return f"http://127.0.0.1:{port}/v1"

    return "http://127.0.0.1:54841/v1"


def ensure_endpoint_ready(base_url: str) -> None:
    health_url = base_url.removesuffix("/v1") + "/health"
    try:
        payload = _http_get_json(health_url, timeout=5.0)
    except Exception as exc:
        raise RuntimeError(f"Local CoPaw model endpoint is unavailable at {base_url}: {exc}") from exc
    if payload.get("status") != "ok":
        raise RuntimeError(f"Local CoPaw model endpoint is not healthy at {base_url}: {payload}")


def discover_model(base_url: str) -> str:
    env_model = __import__("os").environ.get("HUMANIZE_LLM_MODEL", "").strip()
    if env_model:
        return env_model
    payload = _http_get_json(base_url.rstrip("/") + "/models", timeout=10.0)
    models = payload.get("data") or payload.get("models") or []
    if not models:
        raise RuntimeError(f"No models returned by local CoPaw endpoint at {base_url}")
    first = models[0]
    return str(first.get("id") or first.get("model") or first.get("name") or "").strip()


def call_chat(
    *,
    base_url: str,
    model: str,
    system_prompt: str,
    user_prompt: str,
    temperature: float = 0.3,
    max_tokens: int = 300,
) -> dict[str, Any]:
    if base_url.startswith("copaw-active://"):
        return _call_copaw_active_model(
            system_prompt=system_prompt,
            user_prompt=user_prompt,
            temperature=temperature,
            max_tokens=max_tokens,
        )
    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        "temperature": temperature,
        "max_tokens": max_tokens,
    }
    try:
        return _http_post_json(base_url.rstrip("/") + "/chat/completions", payload, timeout=120.0)
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="ignore")
        raise RuntimeError(f"Local CoPaw generation failed: {exc.code} {detail}") from exc


def extract_content(response: dict[str, Any]) -> str:
    choices = response.get("choices") or []
    if not choices:
        raise RuntimeError(f"Local CoPaw generation returned no choices: {response}")
    message = choices[0].get("message") or {}
    content = str(message.get("content") or "").strip()
    if content:
        return content

    reasoning = str(message.get("reasoning_content") or "").strip()
    if not reasoning:
        raise RuntimeError(f"Local CoPaw generation returned empty content: {response}")

    tag_match = re.search(r"<final>\s*(.+?)\s*</final>", reasoning, flags=re.IGNORECASE | re.DOTALL)
    if tag_match:
        return tag_match.group(1).strip()

    marker_match = re.search(r"FINAL_CANDIDATE:\s*(.+)", reasoning)
    if marker_match:
        return marker_match.group(1).strip(" “\"'”")

    labeled_match = re.search(
        r"(?:最终文案|最终回复|最终候选|候选文案|草稿|可能的草稿|建议文案)\s*[：:]\s*(.+)",
        reasoning,
    )
    if labeled_match:
        candidate = labeled_match.group(1).strip(" “\"'”")
        quoted = re.findall(r"[“\"「『]([^”\"」』\n]{8,120})[”\"」』]", candidate)
        if quoted:
            return quoted[-1].strip()
        if candidate:
            return candidate

    line_candidates = []
    for line in reasoning.splitlines():
        stripped = line.strip(" -•\t")
        if not stripped:
            continue
        if stripped.startswith(("比如", "例如")):
            quoted = re.findall(r"[“\"「『]([^”\"」』\n]{8,120})[”\"」』]", stripped)
            if quoted:
                return quoted[-1].strip()
        if any(prefix in stripped for prefix in ("最终选择", "草拟文案", "更简洁", "最终文案", "候选文案")) and "：" in stripped:
            candidate = stripped.split("：", 1)[1].strip(" “\"'”")
            quoted = re.findall(r"[“\"「『]([^”\"」』\n]{8,120})[”\"」』]", candidate)
            if quoted:
                return quoted[-1].strip()
            if candidate:
                line_candidates.append(candidate)
    if line_candidates:
        return line_candidates[-1]

    raise RuntimeError(f"Local CoPaw generation returned empty content and no recoverable final candidate: {response}")


def _constraint_lines(hard_constraints: dict[str, Any]) -> list[str]:
    lines: list[str] = []
    max_chars = hard_constraints.get("max_chars")
    min_chars = hard_constraints.get("min_chars")
    must_include = hard_constraints.get("must_include") or []
    banned = hard_constraints.get("banned_phrases") or []
    if max_chars is not None:
        lines.append(f"- 不超过 {max_chars} 字")
    if min_chars is not None:
        lines.append(f"- 不少于 {min_chars} 字")
    if must_include:
        lines.append("- 必须包含：" + "、".join(str(x) for x in must_include))
    if banned:
        lines.append("- 不要出现：" + "、".join(str(x) for x in banned))
    return lines


def _audience_lines(task: str) -> list[str]:
    lines: list[str] = []
    if "客户" in task:
        lines.extend(
            [
                "- 这是发给客户的消息，不是发给财务或内部同事的消息。",
                "- 不要把客户当成被提问对象，不要反过来向客户索要内部确认。",
            ]
        )
    if "上级" in task or "老板" in task:
        lines.append("- 这是发给上级的消息，不是发给同级或客户的消息。")
    if "面试" in task:
        lines.append("- 这是发给招聘方或面试官的消息，不是发给朋友的消息。")
    if "邮件" in task or "email" in task.lower():
        lines.extend(
            [
                "- 这是邮件回复，不是微信一句话短消息。",
                "- 至少写成完整邮件正文：称呼 + 当前进展 + 下一步时间点。",
                "- 语气要专业、自然、清楚，但不要模板腔。",
            ]
        )
    return lines


def _guidance_lines(
    *,
    profile_name: str,
    strategy_directives: list[str] | None,
    failure_tags: list[str] | None,
) -> list[str]:
    lines: list[str] = []
    profile_desc = PROFILE_LIBRARY.get(profile_name, "").strip()
    if profile_name:
        if profile_desc:
            lines.append(f"- 当前候选风格：{profile_name}。{profile_desc}")
        else:
            lines.append(f"- 当前候选风格：{profile_name}")
    if strategy_directives:
        lines.append("- 当前策略要求：")
        lines.extend(f"  {item}" for item in strategy_directives if item.strip())
    if failure_tags:
        lines.append("- 上一轮失败标签：" + "、".join(failure_tags))
    return lines


def build_generation_prompts(
    *,
    task: str,
    hard_constraints: dict[str, Any],
    original: str,
    mode: str,
    baseline_profile: str = "steady",
    challenger_profile: str = "natural",
    strategy_directives: list[str] | None = None,
    failure_tags: list[str] | None = None,
    revision_mode: str = "rewrite",
) -> tuple[tuple[str, str], tuple[str, str]]:
    constraints = "\n".join(_constraint_lines(hard_constraints)) or "- 无额外硬约束"
    audience = "\n".join(_audience_lines(task))
    baseline_guidance = "\n".join(
        _guidance_lines(
            profile_name=baseline_profile,
            strategy_directives=strategy_directives,
            failure_tags=[],
        ),
    )
    challenger_guidance = "\n".join(
        _guidance_lines(
            profile_name=challenger_profile,
            strategy_directives=strategy_directives,
            failure_tags=failure_tags,
        ),
    )
    system = (
        "你是 humanize 的文案生成器。\n"
        "你的职责是生成中文沟通消息，语气自然、像真人发送、少模板腔。\n"
        "只输出最终文案本身，不要解释，不要标题，不要思考过程，不要项目符号。\n"
        "如果底层模型仍然会输出思考过程，把它压缩到最短，并在最后明确给出唯一最终文案。\n"
        "如果底层模型会把内容拆成 reasoning_content，那么最后一行必须写成 FINAL_CANDIDATE: <最终文案>。"
    )
    baseline_user = (
        f"任务：{task}\n"
        f"默认优化方向：{DEFAULT_GOAL}\n"
        f"硬约束：\n{constraints}\n"
        f"{audience + chr(10) if audience else ''}"
        f"{baseline_guidance + chr(10) if baseline_guidance else ''}"
        "请先生成一个 baseline 版本。\n"
        "要求：\n"
        "- 先求稳，信息完整，语气自然。\n"
        "- 不要过度润色，不要模板腔。\n"
        f"{'- 如果是邮件场景，写成完整邮件回复正文，不要退化成一句短消息。'+ chr(10) if ('邮件' in task or 'email' in task.lower()) else ''}"
        "- 只输出最终文案。\n"
        "- 如果出现思考过程，最后一行必须是 FINAL_CANDIDATE: <最终文案>。"
    )
    if mode == "rewrite":
        baseline_user = (
            f"任务：{task}\n"
            f"默认优化方向：{DEFAULT_GOAL}\n"
            f"硬约束：\n{constraints}\n"
            f"{audience + chr(10) if audience else ''}"
            f"原文：{original}\n"
            "原文已经是 baseline，不需要改写。\n"
            "请原样输出原文，不要添加解释。\n"
            "如果出现思考过程，最后一行必须是 FINAL_CANDIDATE: <最终文案>。"
        )
    if mode == "rewrite" and revision_mode == "repair":
        challenger_user = (
            f"任务：{task}\n"
            f"默认优化方向：{DEFAULT_GOAL}\n"
            f"硬约束：\n{constraints}\n"
            f"{audience + chr(10) if audience else ''}"
            f"{challenger_guidance + chr(10) if challenger_guidance else ''}"
            f"{'原始文本：' + original + chr(10) if original else ''}"
            "这是当前最佳版本（best-so-far），不是原始文本：\n"
            "{baseline}\n"
            "请基于当前最佳版本继续修复，生成下一个 challenger 版本。\n"
            "要求：\n"
            "- 必须满足所有硬约束。\n"
            "- 重点修复上一轮残留问题，不要从原始文本重新整篇改写。\n"
            "- 尽量保留当前最佳版本里已经自然、已经改好的表达。\n"
            "- 如果某一段已经自然，就不要为了改而改。\n"
            "- 不要把已经成型的完整文案压缩成一句空话。\n"
            "- 保留关键信息，不要改变任务语义。\n"
            f"{'- 如果任务是邮件回复，保持完整邮件正文形态，不要回退成聊天短句。'+ chr(10) if ('邮件' in task or 'email' in task.lower()) else ''}"
            "- 只输出最终文案。\n"
            "- 如果出现思考过程，最后一行必须是 FINAL_CANDIDATE: <最终文案>。"
        )
    else:
        challenger_user = (
            f"任务：{task}\n"
            f"默认优化方向：{DEFAULT_GOAL}\n"
            f"硬约束：\n{constraints}\n"
            f"{audience + chr(10) if audience else ''}"
            f"{challenger_guidance + chr(10) if challenger_guidance else ''}"
            f"{'原始文本：' + original + chr(10) if original else ''}"
            "这是当前 baseline：\n"
            "{baseline}\n"
            "请生成一个 challenger 版本。\n"
            "要求：\n"
            "- 必须满足所有硬约束。\n"
            "- 比 baseline 更像真人自然发送的中文沟通消息。\n"
            "- 保留关键信息，不要改变任务语义。\n"
            "- 与 baseline 要有明确差异，不要只改一两个字。\n"
            f"{'- 如果任务是邮件回复，保持邮件正文形态，至少包含称呼、进展、下一步时间点，不要缩成一句话。'+ chr(10) if ('邮件' in task or 'email' in task.lower()) else ''}"
            "- 只输出最终文案。\n"
            "- 如果出现思考过程，最后一行必须是 FINAL_CANDIDATE: <最终文案>。"
        )
    return (system, baseline_user), (system, challenger_user)


path: scripts/parse_user_brief.py
content: from __future__ import annotations

import argparse
import json
import re
from pathlib import Path
from typing import Any

import yaml

from runtime_common import DEFAULT_GOAL

LABEL_MAP = {
    "task": ("任务", "task"),
    "constraints": ("约束", "constraints"),
    "original": ("原文", "原稿", "正文", "draft", "message"),
    "goal": ("目标", "goal"),
}
INLINE_LABELS = (
    ("任务", "task"),
    ("约束", "constraints"),
    ("原文", "original"),
    ("原稿", "original"),
    ("正文", "original"),
    ("目标", "goal"),
    ("task", "task"),
    ("constraints", "constraints"),
    ("goal", "goal"),
    ("draft", "original"),
    ("message", "original"),
)

GENERIC_TASK = "优化这条中文沟通消息"
SCENARIO_KEYWORDS = (
    "回复",
    "微信",
    "消息",
    "邮件",
    "沟通",
    "客户",
    "上级",
    "老板",
    "面试",
    "售后",
    "文案",
    "自媒体",
    "小红书",
    "公众号",
    "朋友圈",
    "公告",
    "社群",
    "差评",
    "评价",
    "口播",
    "脚本",
    "短视频",
)
MESSAGE_HINTS = ("您好", "你好", "这边", "这次", "辛苦", "麻烦", "谢谢", "感谢", "明天下午", "财务", "方便", "抱歉")
INSTRUCTION_HINTS = ("任务", "约束", "目标", "保留", "控制在", "不超过", "生成", "优化", "更像真人", "别太", "不要")


def clean_text(text: str) -> str:
    return re.sub(r"\n{3,}", "\n\n", text.strip())


def split_lines(text: str) -> list[str]:
    return [line.rstrip() for line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n")]


def is_skill_invocation(line: str) -> bool:
    stripped = line.strip()
    if not stripped:
        return False
    lowered = stripped.lower()
    return lowered.startswith("用 humanize") or lowered.startswith("humanize ") or "用 humanize 帮我" in lowered


def looks_like_message(text: str) -> bool:
    stripped = clean_text(text)
    if not stripped:
        return False
    if any(hint in stripped for hint in MESSAGE_HINTS):
        return True
    if sum(1 for char in "，。！？；" if char in stripped) >= 1 and len(stripped) >= 18:
        return True
    if any(hint in stripped for hint in INSTRUCTION_HINTS):
        return False
    return False


def label_key(line: str) -> tuple[str | None, str]:
    stripped = line.strip()
    for key, labels in LABEL_MAP.items():
        for label in labels:
            match = re.match(rf"^{re.escape(label)}\s*[:：]?\s*(.*)$", stripped, flags=re.IGNORECASE)
            if match:
                return key, match.group(1).strip()
    return None, stripped


def parse_labeled_sections(text: str) -> tuple[dict[str, str], list[str]]:
    sections: dict[str, list[str]] = {}
    unclaimed: list[str] = []
    current: str | None = None

    for raw_line in split_lines(text):
        stripped = raw_line.strip()
        if not stripped:
            if current and sections.get(current):
                sections[current].append("")
            continue
        key, remainder = label_key(raw_line)
        if key:
            current = key
            sections.setdefault(key, [])
            if remainder:
                sections[key].append(remainder)
            continue
        if current:
            sections.setdefault(current, []).append(stripped)
        else:
            unclaimed.append(stripped)

    flattened = {
        key: clean_text("\n".join(value))
        for key, value in sections.items()
        if clean_text("\n".join(value))
    }
    return flattened, unclaimed


def parse_inline_sections(text: str) -> dict[str, str]:
    pattern = re.compile(
        r"(任务|约束|原文|原稿|正文|目标|task|constraints|goal|draft|message)\s*[:：]",
        flags=re.IGNORECASE,
    )
    matches = list(pattern.finditer(text))
    if not matches:
        return {}

    sections: dict[str, str] = {}
    for index, match in enumerate(matches):
        raw_label = match.group(1).lower()
        key = None
        for label, canonical in INLINE_LABELS:
            if raw_label == label.lower():
                key = canonical
                break
        if key is None:
            continue
        start = match.end()
        end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
        value = clean_text(text[start:end].strip(" \n\t。；;"))
        if value and key not in sections:
            sections[key] = value
    return sections


def extract_quoted_terms(text: str) -> list[str]:
    return [
        item.strip()
        for item in re.findall(r"[“\"'「『](.+?)[”\"'」』]", text)
        if item.strip()
    ]


def split_plain_constraint_terms(text: str) -> list[str]:
    fragment = re.sub(r"(控制在|限制在|不超过|别超过|最多|至少|不要|别用|避免).*$", "", text).strip()
    fragment = fragment.strip(" ，。；;:：")
    if not fragment:
        return []
    fragment = re.sub(r"^(这些|这几个|以下|关键词|词语)", "", fragment).strip()
    candidates = re.split(r"[、，,和及与/]\s*", fragment)
    output: list[str] = []
    for item in candidates:
        value = item.strip(" “\"'”‘’()（）")
        if not value:
            continue
        if len(value) > 16:
            continue
        if any(token in value for token in ("保留", "必须", "包含", "控制在", "不要", "避免")):
            continue
        output.append(value)
    return unique_keep_order(output)


def is_soft_constraint_term(value: str) -> bool:
    soft_markers = (
        "核心观点",
        "逻辑结构",
        "结构",
        "原文",
        "语气",
        "口语",
        "口语化",
        "书面语",
        "减少书面语",
        "说教感",
        "ai腔",
        "AI腔",
        "模板腔",
        "自然",
        "风格",
        "观点",
        "内容",
        "真人会说",
    )
    return any(marker in value for marker in soft_markers)


def unique_keep_order(values: list[str]) -> list[str]:
    output: list[str] = []
    seen: set[str] = set()
    for value in values:
        if value not in seen:
            seen.add(value)
            output.append(value)
    return output


def infer_max_chars(text: str) -> int | None:
    patterns = [
        r"(?:控制在|限制在|不超过|别超过|最多)\s*(\d+)\s*字",
        r"(\d+)\s*字内",
    ]
    for pattern in patterns:
        match = re.search(pattern, text)
        if match:
            return int(match.group(1))
    return None


def infer_must_include(text: str) -> list[str]:
    matches: list[str] = []
    for match in re.finditer(r"(?:保留|带上|包含|必须提到)(.{0,100})", text):
        fragment = match.group(1)
        quoted = extract_quoted_terms(fragment)
        matches.extend(quoted or split_plain_constraint_terms(fragment))
    return unique_keep_order([item for item in matches if not is_soft_constraint_term(item)])


def infer_banned_phrases(text: str) -> list[str]:
    matches: list[str] = []
    for match in re.finditer(r"(?:不要|别用|不要出现|避免|别写)(.{0,100})", text):
        fragment = match.group(1)
        quoted = extract_quoted_terms(fragment)
        matches.extend(quoted or split_plain_constraint_terms(fragment))
    return unique_keep_order(matches)


def infer_task(unclaimed: list[str], original: str, text: str) -> str:
    for line in unclaimed:
        stripped = line.strip()
        if not stripped or is_skill_invocation(stripped):
            continue
        if any(keyword in stripped for keyword in SCENARIO_KEYWORDS):
            return stripped.rstrip("：:")
    for line in split_lines(text):
        stripped = line.strip()
        if not stripped or not is_skill_invocation(stripped):
            continue
        normalized = re.sub(r"^用\s*humanize\s*帮我", "", stripped, flags=re.IGNORECASE).strip(" ：:")
        normalized = re.split(
            r"(?:原文|原稿|正文|约束|要求|需求|目标|task|constraints|goal|draft|message)\s*[:：]",
            normalized,
            maxsplit=1,
            flags=re.IGNORECASE,
        )[0].strip(" ：:")
        humanize_suffix = r"改得更像真人(?:会?(?:说|写|发))?(?:的话|的|话)?"
        capture = re.match(
            rf"^把这(?:段|封|篇|条)?(.+?)(?:{humanize_suffix}|改得更像真人一点|改得更自然(?:一点)?|改得更像人话|优化一下|润色一下)$",
            normalized,
        )
        if capture:
            normalized = capture.group(1).strip(" ，。,:：")
        normalized = re.sub(
            rf"({humanize_suffix}|改得更像真人一点|改得更自然(?:一点)?|改得更像人话|优化一下|优化这条中文沟通消息|生成并优化一条中文沟通消息|润色一下)",
            "",
            normalized,
        )
        normalized = re.sub(r"[，,。]\s*(?:要求|需求)?\s*(保留|带上|包含|必须提到|控制在|限制在|不超过|别超过|最多).*$", "", normalized)
        normalized = re.sub(r"\s*(?:要求|需求)?\s*(保留|带上|包含|必须提到|控制在|限制在|不超过|别超过|最多).*$", "", normalized)
        normalized = re.sub(r"[，,。]?\s*(?:要求|需求)$", "", normalized)
        normalized = normalized.strip(" ，。,:：")
        if normalized and any(keyword in normalized for keyword in SCENARIO_KEYWORDS):
            return normalized
    if "客户" in original:
        return "给客户发送中文沟通回复"
    if "面试" in original:
        return "撰写中文面试沟通回复"
    return GENERIC_TASK


def infer_original(text: str, sections: dict[str, str], unclaimed: list[str]) -> str:
    explicit = sections.get("original", "").strip()
    if explicit:
        return explicit
    if sections:
        return ""

    lines = split_lines(text)
    for line in lines:
        stripped = line.strip()
        if not stripped or not is_skill_invocation(stripped):
            continue
        if "：" in stripped or ":" in stripped:
            head, tail = re.split(r"[:：]", stripped, maxsplit=1)
            if any(token in head for token in ("背景", "要求", "约束", "任务", "保留", "控制在", "限制在")):
                continue
            if "背景" in stripped and any(token in stripped for token in ("要求", "保留", "控制在", "限制在")):
                continue
            tail = tail.strip()
            if tail and looks_like_message(tail):
                return tail

    residual: list[str] = []
    for item in unclaimed:
        stripped = item.strip()
        if not stripped or is_skill_invocation(stripped):
            continue
        if re.search(r"(?:\d+\s*字内|控制在|不超过|保留|避免|不要)", stripped):
            continue
        residual.append(stripped)

    if residual:
        joined = clean_text("\n".join(residual))
        if looks_like_message(joined):
            return joined

    return ""


def build_payload(raw_text: str) -> dict[str, Any]:
    text = clean_text(raw_text)
    sections, unclaimed = parse_labeled_sections(text)
    inline_sections = parse_inline_sections(text)
    for key, value in inline_sections.items():
        sections.setdefault(key, value)
    original = infer_original(text, sections, unclaimed)
    constraints_text = sections.get("constraints", "")
    goal = clean_text(sections.get("goal", ""))
    task = clean_text(sections.get("task", "")) or infer_task(unclaimed, original, text)

    infer_text = "\n".join([constraints_text, *unclaimed]).strip()
    max_chars = infer_max_chars(infer_text)
    must_include = infer_must_include(infer_text)
    banned_phrases = infer_banned_phrases(infer_text)

    hard_constraints: dict[str, Any] = {}
    if max_chars is not None:
        hard_constraints["max_chars"] = max_chars
    if must_include:
        hard_constraints["must_include"] = must_include
    if banned_phrases:
        hard_constraints["banned_phrases"] = banned_phrases

    input_mode = "structured" if sections.get("original") or sections.get("task") or sections.get("constraints") else "loose"
    session_mode = "rewrite" if original else "generate"
    spec: dict[str, Any] = {
        "task": task or GENERIC_TASK,
        "hard_constraints": hard_constraints,
        "evaluator": {
            "minimum_improvement": 0.015,
        },
    }
    if goal and goal != DEFAULT_GOAL:
        spec["goal"] = goal

    return {
        "input_mode": input_mode,
        "session_mode": session_mode,
        "parsed": {
            "task": spec["task"],
            "goal": spec.get("goal", ""),
            "hard_constraints": hard_constraints,
            "original": original,
        },
        "spec": spec,
    }


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Normalize user input into a humanize spec and original draft.",
    )
    parser.add_argument("--text", default=None)
    parser.add_argument("--input", type=Path, default=None)
    parser.add_argument("--json-output", type=Path, default=None)
    parser.add_argument("--spec-output", type=Path, default=None)
    parser.add_argument("--source-output", type=Path, default=None)
    args = parser.parse_args()

    if args.text is None and args.input is None:
        raise ValueError("Provide --text or --input")
    raw_text = args.text if args.text is not None else args.input.read_text(encoding="utf-8")
    payload = build_payload(raw_text)

    if args.spec_output:
        args.spec_output.parent.mkdir(parents=True, exist_ok=True)
        args.spec_output.write_text(
            yaml.safe_dump(
                payload["spec"],
                allow_unicode=True,
                sort_keys=False,
                default_flow_style=False,
            ),
            encoding="utf-8",
        )
    if args.source_output:
        args.source_output.parent.mkdir(parents=True, exist_ok=True)
        args.source_output.write_text(payload["parsed"]["original"], encoding="utf-8")
    if args.json_output:
        args.json_output.parent.mkdir(parents=True, exist_ok=True)
        args.json_output.write_text(
            json.dumps(payload, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )

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


if __name__ == "__main__":
    main()


path: scripts/prepare_run.py
content: from __future__ import annotations

import argparse
import json
import re
from datetime import datetime
from pathlib import Path
from typing import Any

import yaml

from parse_user_brief import build_payload


def slugify(text: str) -> str:
    compact = re.sub(r"[^\w\u4e00-\u9fff]+", "-", text.strip().lower())
    compact = re.sub(r"-{2,}", "-", compact).strip("-")
    return compact[:40] or "run"


def create_run_dir(output_root: Path, task: str) -> Path:
    run_id = datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + slugify(task)
    run_dir = output_root / run_id
    run_dir.mkdir(parents=True, exist_ok=False)
    (run_dir / "rounds").mkdir(exist_ok=True)
    return run_dir


def write_text(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")


def build_session_plan(payload: dict[str, Any], run_dir: Path) -> dict[str, Any]:
    parsed = payload["parsed"]
    session_mode = payload["session_mode"]
    if session_mode == "generate":
        summary = "No original draft was provided. Generate a baseline from task and constraints first, then generate a challenger and compare them."
    else:
        summary = "An original draft was provided. Use it as baseline, then generate one challenger and compare them."
    return {
        "session_mode": session_mode,
        "input_mode": payload["input_mode"],
        "task": parsed["task"],
        "hard_constraints": parsed["hard_constraints"],
        "goal_override": parsed["goal"],
        "original_provided": bool(parsed["original"]),
        "baseline_strategy": "use_original_as_baseline" if session_mode == "rewrite" else "generate_baseline_from_task",
        "run_dir": str(run_dir.resolve()),
        "spec_path": str((run_dir / "spec.yaml").resolve()),
        "source_path": str((run_dir / "source.txt").resolve()),
        "baseline_path": str((run_dir / "baseline.txt").resolve()),
        "challenger_path": str((run_dir / "challenger.txt").resolve()),
        "compare_result_path": str((run_dir / "compare-result.json").resolve()),
        "report_html_path": str((run_dir / "report.html").resolve()),
        "summary": summary,
        "next_actions": (
            [
                "Generate one baseline draft from the task and constraints.",
                "Generate one challenger draft that is intentionally different.",
                "Run scripts/run_session.py with the baseline and challenger.",
            ]
            if session_mode == "generate"
            else [
                "Treat source.txt as baseline.",
                "Generate one challenger draft that improves naturalness without breaking constraints.",
                "Run scripts/run_session.py with the baseline and challenger.",
            ]
        ),
    }


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Prepare a visible humanize run from raw user input.",
    )
    parser.add_argument("--text", default=None)
    parser.add_argument("--input", type=Path, default=None)
    parser.add_argument("--run-dir", type=Path, default=None)
    parser.add_argument("--output-root", type=Path, default=Path("./runs"))
    args = parser.parse_args()

    if args.text is None and args.input is None:
        raise ValueError("Provide --text or --input")

    raw_text = args.text if args.text is not None else args.input.read_text(encoding="utf-8")
    payload = build_payload(raw_text)
    task = str(payload["parsed"]["task"] or "run")
    run_dir = args.run_dir.resolve() if args.run_dir else create_run_dir(args.output_root.resolve(), task)
    run_dir.mkdir(parents=True, exist_ok=True)
    (run_dir / "rounds").mkdir(exist_ok=True)

    write_text(
        run_dir / "spec.yaml",
        yaml.safe_dump(
            payload["spec"],
            allow_unicode=True,
            sort_keys=False,
            default_flow_style=False,
        ),
    )
    write_text(run_dir / "user-brief.txt", raw_text.strip() + "\n")
    write_text(run_dir / "source.txt", payload["parsed"]["original"])
    if payload["session_mode"] == "rewrite":
        write_text(run_dir / "baseline.txt", payload["parsed"]["original"])

    session_plan = build_session_plan(payload, run_dir)
    write_text(
        run_dir / "session-plan.json",
        json.dumps(session_plan, ensure_ascii=False, indent=2),
    )
    write_text(
        run_dir / "parse-result.json",
        json.dumps(payload, ensure_ascii=False, indent=2),
    )

    print(
        json.dumps(
            {
                "run_dir": str(run_dir.resolve()),
                "session_mode": payload["session_mode"],
                "input_mode": payload["input_mode"],
                "task": payload["parsed"]["task"],
                "source_path": str((run_dir / "source.txt").resolve()),
                "spec_path": str((run_dir / "spec.yaml").resolve()),
                "session_plan_path": str((run_dir / "session-plan.json").resolve()),
                "baseline_path": str((run_dir / "baseline.txt").resolve()) if payload["session_mode"] == "rewrite" else "",
            },
            ensure_ascii=False,
            indent=2,
        ),
    )


if __name__ == "__main__":
    main()


path: scripts/record_round.py
content: from __future__ import annotations

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


def main() -> None:
    parser = argparse.ArgumentParser(description="Append one compare result to a run log.")
    parser.add_argument("--run-dir", required=True, type=Path)
    parser.add_argument("--result", required=True, type=Path)
    args = parser.parse_args()

    result = json.loads(args.result.read_text(encoding="utf-8"))
    result["recorded_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")

    log_path = args.run_dir / "rounds.jsonl"
    with log_path.open("a", encoding="utf-8") as fh:
        fh.write(json.dumps(result, ensure_ascii=False) + "\n")

    round_dir = args.run_dir / "rounds"
    round_dir.mkdir(exist_ok=True)
    index = len(list(round_dir.glob("round-*.json"))) + 1
    snapshot_path = round_dir / f"round-{index:03d}.json"
    snapshot_path.write_text(
        json.dumps(result, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    print(
        json.dumps(
            {
                "log_path": str(log_path.resolve()),
                "snapshot_path": str(snapshot_path.resolve()),
                "round_index": index,
            },
            ensure_ascii=False,
            indent=2,
        ),
    )


if __name__ == "__main__":
    main()


path: scripts/render_run_report.py
content: from __future__ import annotations

import argparse
import html
import json
from pathlib import Path
from typing import Any


def load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def read_text(path: Path) -> str:
    if not path.exists():
        return ""
    return path.read_text(encoding="utf-8").strip()


def pct(value: float) -> str:
    return f"{value * 100:.1f}%"


def badge_class(decision: str) -> str:
    return "keep" if decision == "keep" else "discard"


def build_markdown(compare: dict[str, Any], baseline_text: str, challenger_text: str) -> str:
    baseline = compare["baseline"]
    challenger = compare["challenger"]
    baseline_breakdown = baseline.get("rule_breakdown") or {}
    challenger_breakdown = challenger.get("rule_breakdown") or {}
    lines: list[str] = []
    lines.append(f"# Optimization Report")
    lines.append("")
    lines.append(f"- Decision: `{compare['decision']}`")
    lines.append(f"- Winner: `{compare['winner']}`")
    lines.append(f"- Reason: {compare['reason']}")
    lines.append(f"- Delta: `{compare['delta']}`")
    lines.append(f"- Margin: `{compare['margin']}`")
    lines.append("")
    lines.append("## Score Summary")
    lines.append("")
    lines.append(f"- Baseline final score: `{baseline['final_score']}`")
    lines.append(f"- Challenger final score: `{challenger['final_score']}`")
    lines.append(f"- Baseline model score: `{baseline['model_score']}`")
    lines.append(f"- Challenger model score: `{challenger['model_score']}`")
    lines.append(f"- Baseline rule score: `{baseline['rule_score']}`")
    lines.append(f"- Challenger rule score: `{challenger['rule_score']}`")
    lines.append("")
    lines.append("## Baseline")
    lines.append("")
    lines.append("```text")
    lines.append(baseline_text)
    lines.append("```")
    lines.append("")
    lines.append("## Challenger")
    lines.append("")
    lines.append("```text")
    lines.append(challenger_text)
    lines.append("```")
    lines.append("")
    lines.append("## Baseline Notes")
    lines.append("")
    if baseline.get("notes"):
        lines.extend(f"- {note}" for note in baseline["notes"])
    else:
        lines.append("- None")
    lines.append("")
    lines.append("## Challenger Notes")
    lines.append("")
    if challenger.get("notes"):
        lines.extend(f"- {note}" for note in challenger["notes"])
    else:
        lines.append("- None")
    lines.append("")
    lines.append("## Rule Breakdown")
    lines.append("")
    lines.append("| Metric | Baseline | Challenger |")
    lines.append("| --- | ---: | ---: |")
    keys = list(dict.fromkeys(list(baseline_breakdown.keys()) + list(challenger_breakdown.keys())))
    for key in keys:
        lines.append(
            f"| {key} | {float(baseline_breakdown.get(key, 0.0)):.6f} | {float(challenger_breakdown.get(key, 0.0)):.6f} |",
        )
    lines.append("")
    return "\n".join(lines)


def build_score_bar(value: float, color: str) -> str:
    width = max(2, min(100, int(round(value * 100))))
    return (
        '<div class="bar-track">'
        f'<div class="bar-fill {color}" style="width:{width}%"></div>'
        "</div>"
    )


def build_html(compare: dict[str, Any], baseline_text: str, challenger_text: str) -> str:
    baseline = compare["baseline"]
    challenger = compare["challenger"]
    decision = compare["decision"]
    baseline_breakdown = baseline.get("rule_breakdown") or {}
    challenger_breakdown = challenger.get("rule_breakdown") or {}
    rows = []
    keys = list(dict.fromkeys(list(baseline_breakdown.keys()) + list(challenger_breakdown.keys())))
    for key in keys:
        rows.append(
            "<tr>"
            f"<td>{html.escape(str(key))}</td>"
            f"<td>{float(baseline_breakdown.get(key, 0.0)):.6f}</td>"
            f"<td>{float(challenger_breakdown.get(key, 0.0)):.6f}</td>"
            "</tr>",
        )
    baseline_notes = "".join(
        f"<li>{html.escape(note)}</li>" for note in baseline["notes"]
    ) or "<li>None</li>"
    challenger_notes = "".join(
        f"<li>{html.escape(note)}</li>" for note in challenger["notes"]
    ) or "<li>None</li>"
    return f"""<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Optimization Report</title>
  <style>
    :root {{
      --bg: #f5f1e8;
      --panel: #fffaf0;
      --ink: #1a1a17;
      --muted: #5c584f;
      --line: #d9d1c0;
      --accent: #183153;
      --green: #275d38;
      --red: #903030;
      --gold: #b2862f;
    }}
    * {{ box-sizing: border-box; }}
    body {{
      margin: 0;
      padding: 32px;
      background:
        radial-gradient(circle at top left, rgba(24,49,83,0.08), transparent 32%),
        radial-gradient(circle at bottom right, rgba(178,134,47,0.10), transparent 28%),
        var(--bg);
      color: var(--ink);
      font: 15px/1.6 "Iowan Old Style", "Palatino Linotype", serif;
    }}
    .shell {{
      max-width: 1160px;
      margin: 0 auto;
      display: grid;
      gap: 20px;
    }}
    .hero, .panel {{
      background: rgba(255,250,240,0.92);
      border: 1px solid var(--line);
      border-radius: 18px;
      box-shadow: 0 14px 34px rgba(24, 28, 34, 0.08);
      backdrop-filter: blur(10px);
    }}
    .hero {{
      padding: 28px;
      display: grid;
      gap: 12px;
    }}
    .eyebrow {{
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.12em;
      font-size: 12px;
    }}
    h1 {{
      margin: 0;
      font-size: clamp(30px, 5vw, 54px);
      line-height: 1.05;
    }}
    .meta {{
      display: flex;
      flex-wrap: wrap;
      gap: 12px;
      color: var(--muted);
    }}
    .badge {{
      display: inline-flex;
      align-items: center;
      gap: 8px;
      width: fit-content;
      padding: 7px 12px;
      border-radius: 999px;
      font-weight: 700;
      font-size: 13px;
    }}
    .badge.keep {{
      background: rgba(39,93,56,0.12);
      color: var(--green);
      border: 1px solid rgba(39,93,56,0.25);
    }}
    .badge.discard {{
      background: rgba(144,48,48,0.10);
      color: var(--red);
      border: 1px solid rgba(144,48,48,0.22);
    }}
    .grid {{
      display: grid;
      gap: 20px;
    }}
    .grid.metrics {{
      grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    }}
    .metric {{
      padding: 18px;
    }}
    .metric .label {{
      color: var(--muted);
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: 0.08em;
    }}
    .metric .value {{
      margin-top: 8px;
      font-size: 34px;
      line-height: 1;
      font-weight: 700;
    }}
    .comparison {{
      grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    }}
    .panel {{
      padding: 22px;
    }}
    .panel h2, .panel h3 {{
      margin-top: 0;
      margin-bottom: 10px;
    }}
    .copy {{
      white-space: pre-wrap;
      background: rgba(24,49,83,0.05);
      border: 1px solid rgba(24,49,83,0.08);
      border-radius: 12px;
      padding: 16px;
      min-height: 140px;
    }}
    .score-line {{
      display: grid;
      gap: 6px;
      margin-bottom: 14px;
    }}
    .score-head {{
      display: flex;
      justify-content: space-between;
      gap: 12px;
      font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
      font-size: 13px;
    }}
    .bar-track {{
      height: 10px;
      border-radius: 999px;
      background: rgba(24,49,83,0.08);
      overflow: hidden;
    }}
    .bar-fill {{
      height: 100%;
      border-radius: 999px;
    }}
    .bar-fill.blue {{ background: linear-gradient(90deg, #204c8c, #3e74c5); }}
    .bar-fill.gold {{ background: linear-gradient(90deg, #9c7425, #c9a84d); }}
    .notes {{
      margin: 0;
      padding-left: 18px;
    }}
    table {{
      width: 100%;
      border-collapse: collapse;
      font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
      font-size: 13px;
    }}
    th, td {{
      border-top: 1px solid var(--line);
      padding: 10px 8px;
      text-align: left;
    }}
    th {{
      color: var(--muted);
      font-weight: 700;
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: 0.08em;
    }}
    .footer {{
      color: var(--muted);
      font-size: 12px;
    }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <div class="eyebrow">ZH Communication Evolver</div>
      <h1>Optimization Report</h1>
      <div class="badge {badge_class(decision)}">{html.escape(decision.upper())} · {html.escape(compare['winner'])}</div>
      <div class="meta">
        <span>Reason: {html.escape(compare['reason'])}</span>
        <span>Delta: {compare['delta']}</span>
        <span>Margin: {compare['margin']}</span>
      </div>
    </section>

    <section class="grid metrics">
      <div class="panel metric">
        <div class="label">Baseline Score</div>
        <div class="value">{baseline['final_score']:.6f}</div>
      </div>
      <div class="panel metric">
        <div class="label">Challenger Score</div>
        <div class="value">{challenger['final_score']:.6f}</div>
      </div>
      <div class="panel metric">
        <div class="label">Score Delta</div>
        <div class="value">{compare['delta']:.6f}</div>
      </div>
      <div class="panel metric">
        <div class="label">Decision</div>
        <div class="value">{html.escape(compare['decision'])}</div>
      </div>
    </section>

    <section class="grid comparison">
      <article class="panel">
        <h2>Baseline</h2>
        <div class="score-line">
          <div class="score-head"><span>Final</span><span>{pct(baseline['final_score'])}</span></div>
          {build_score_bar(baseline['final_score'], 'blue')}
        </div>
        <div class="score-line">
          <div class="score-head"><span>Model</span><span>{pct(baseline['model_score'])}</span></div>
          {build_score_bar(baseline['model_score'], 'gold')}
        </div>
        <div class="score-line">
          <div class="score-head"><span>Rules</span><span>{pct(baseline['rule_score'])}</span></div>
          {build_score_bar(baseline['rule_score'], 'gold')}
        </div>
        <div class="copy">{html.escape(baseline_text)}</div>
        <h3>Notes</h3>
        <ul class="notes">{baseline_notes}</ul>
      </article>

      <article class="panel">
        <h2>Challenger</h2>
        <div class="score-line">
          <div class="score-head"><span>Final</span><span>{pct(challenger['final_score'])}</span></div>
          {build_score_bar(challenger['final_score'], 'blue')}
        </div>
        <div class="score-line">
          <div class="score-head"><span>Model</span><span>{pct(challenger['model_score'])}</span></div>
          {build_score_bar(challenger['model_score'], 'gold')}
        </div>
        <div class="score-line">
          <div class="score-head"><span>Rules</span><span>{pct(challenger['rule_score'])}</span></div>
          {build_score_bar(challenger['rule_score'], 'gold')}
        </div>
        <div class="copy">{html.escape(challenger_text)}</div>
        <h3>Notes</h3>
        <ul class="notes">{challenger_notes}</ul>
      </article>
    </section>

    <section class="panel">
      <h2>Rule Breakdown</h2>
      <table>
        <thead>
          <tr><th>Metric</th><th>Baseline</th><th>Challenger</th></tr>
        </thead>
        <tbody>
          {''.join(rows)}
        </tbody>
      </table>
    </section>

    <section class="panel footer">
      <div>Spec: {html.escape(compare['spec_path'])}</div>
      <div>Baseline file: {html.escape(compare['baseline']['path'])}</div>
      <div>Challenger file: {html.escape(compare['challenger']['path'])}</div>
    </section>
  </main>
</body>
</html>
"""


def main() -> None:
    parser = argparse.ArgumentParser(description="Render markdown and HTML for one optimization run.")
    parser.add_argument("--run-dir", required=True, type=Path)
    args = parser.parse_args()

    run_dir = args.run_dir.resolve()
    compare_path = run_dir / "compare-result.json"
    baseline_path = run_dir / "baseline.txt"
    challenger_path = run_dir / "challenger.txt"
    if not compare_path.exists():
        raise FileNotFoundError(f"Missing compare-result.json in {run_dir}")

    compare = load_json(compare_path)
    baseline_text = read_text(baseline_path)
    challenger_text = read_text(challenger_path)

    md = build_markdown(compare, baseline_text, challenger_text)
    html_doc = build_html(compare, baseline_text, challenger_text)

    md_path = run_dir / "report.md"
    html_path = run_dir / "report.html"
    md_path.write_text(md, encoding="utf-8")
    html_path.write_text(html_doc, encoding="utf-8")

    print(
        json.dumps(
            {
                "run_dir": str(run_dir),
                "markdown_report": str(md_path),
                "html_report": str(html_path),
            },
            ensure_ascii=False,
            indent=2,
        ),
    )


if __name__ == "__main__":
    main()


path: scripts/run_from_brief.py
content: from __future__ import annotations

import argparse
import json
import os
import re
import sys
from collections import Counter
from datetime import datetime, timezone
from difflib import SequenceMatcher
from pathlib import Path
from typing import Any

import yaml

from runtime_common import reexec_into_runtime

reexec_into_runtime()

from local_generation import (  # noqa: E402
    build_generation_prompts,
    call_chat,
    discover_generation_backend,
    extract_content,
)
from parse_user_brief import build_payload  # noqa: E402
from prepare_run import create_run_dir  # noqa: E402
from render_run_report import build_html, build_markdown  # noqa: E402
from scoring_core import DEFAULT_TEMPLATE_PHRASES, dump_score_json, score_candidate  # noqa: E402
from strategy_state import (  # noqa: E402
    choose_profiles,
    evolve_after_attempts,
    extract_failure_tags,
    load_state,
    save_state,
    snapshot_state,
    state_directives,
)

DEFAULT_MAX_ROUNDS = 3
MAX_ALLOWED_ROUNDS = 5


def write_text(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")


def write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


def read_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def candidate_rank_key(candidate: dict[str, Any]) -> tuple[int, float, float]:
    score = candidate.get("score") or {}
    return (
        1 if not score.get("hard_fail") else 0,
        float(score.get("final_score") or 0.0),
        float(score.get("model_score") or 0.0),
    )


def clamp_max_rounds(value: int | str | None) -> int:
    if value is None or str(value).strip() == "":
        return DEFAULT_MAX_ROUNDS
    try:
        parsed = int(value)
    except (TypeError, ValueError):
        return DEFAULT_MAX_ROUNDS
    return max(1, min(MAX_ALLOWED_ROUNDS, parsed))


def derive_run_budget(task: str, session_mode: str, max_rounds_override: int | None = None) -> dict[str, int]:
    lowered = task.lower()
    is_email = "邮件" in task or "email" in lowered
    is_short_chat = "微信" in task or "wechat" in lowered or any(
        marker in task for marker in ("App", "APP", "推送", "直播课", "消息", "飞书")
    )
    max_rounds = clamp_max_rounds(max_rounds_override if max_rounds_override is not None else os.environ.get("HUMANIZE_MAX_ROUNDS"))
    if session_mode == "rewrite" and is_email:
        return {"challenger_count": 1, "max_rounds": max_rounds}
    if session_mode == "rewrite":
        return {"challenger_count": 2, "max_rounds": max_rounds}
    if is_email:
        return {"challenger_count": 2, "max_rounds": max_rounds}
    if is_short_chat:
        return {"challenger_count": 2, "max_rounds": max_rounds}
    return {"challenger_count": 2, "max_rounds": max_rounds}


def pick_best_candidate(candidates: list[dict[str, Any]]) -> dict[str, Any]:
    valid = [item for item in candidates if item.get("score") and not item["score"].get("hard_fail")]
    pool = valid or [item for item in candidates if item.get("score")]
    if not pool:
        raise RuntimeError("No candidate produced a scorable output")
    return max(pool, key=candidate_rank_key)


def aggregate_failure_tags(candidates: list[dict[str, Any]], selected: dict[str, Any]) -> list[str]:
    counts: Counter[str] = Counter()
    for candidate in candidates:
        for tag in candidate.get("failure_tags") or []:
            counts[tag] += 1
    ordered: list[str] = []
    for tag in selected.get("failure_tags") or []:
        if tag not in ordered:
            ordered.append(tag)
    for tag, _ in counts.most_common():
        if tag not in ordered:
            ordered.append(tag)
    return ordered


QUALITY_GATE_RETRY_TAGS = {
    "too_similar",
    "copied_baseline",
    "source_template_carryover",
    "placeholder_output",
    "bad_splice",
    "overcompressed",
}


def quality_gate_tags(candidate: dict[str, Any]) -> list[str]:
    ordered: list[str] = []
    seen: set[str] = set()
    for tag in candidate.get("failure_tags") or []:
        if tag in QUALITY_GATE_RETRY_TAGS and tag not in seen:
            seen.add(tag)
            ordered.append(tag)
    return ordered


def retryable_quality_tags(tags: list[str]) -> list[str]:
    ordered: list[str] = []
    for tag in tags:
        if tag in QUALITY_GATE_RETRY_TAGS and tag not in ordered:
            ordered.append(tag)
    return ordered


def should_continue_refinement(
    *,
    selected: dict[str, Any],
    delta: float,
    margin: float,
    round_number: int,
    max_rounds: int,
) -> tuple[bool, list[str]]:
    blockers = quality_gate_tags(selected)
    if round_number >= max_rounds:
        return False, blockers
    score = selected.get("score") or {}
    if bool(score.get("hard_fail")) or delta < margin:
        return False, blockers
    return bool(blockers), blockers


def build_trace_markdown(trace_payload: dict[str, Any]) -> str:
    lines: list[str] = []
    lines.append("# Evolution Trace")
    lines.append("")
    lines.append(f"- Session mode: `{trace_payload['session_mode']}`")
    lines.append(f"- Input mode: `{trace_payload['input_mode']}`")
    lines.append(f"- Max rounds: `{trace_payload.get('run_budget', {}).get('max_rounds', DEFAULT_MAX_ROUNDS)}`")
    lines.append(f"- Total rounds: `{len(trace_payload['rounds'])}`")
    lines.append(f"- Improved any round: `{trace_payload['improved_any']}`")
    lines.append("")
    for round_payload in trace_payload["rounds"]:
        lines.append(f"## Round {round_payload['round']}")
        lines.append("")
        lines.append(f"- Baseline score at round start: `{round_payload['baseline_score']['final_score']}`")
        lines.append(
            f"- Revision mode: `{round_payload.get('revision_mode', 'rewrite')}` "
            f"({localize_revision_mode(round_payload.get('revision_mode', 'rewrite'))})",
        )
        lines.append(
            f"- Base text: `{round_payload.get('base_text_kind', 'source')}` "
            f"({localize_base_text_kind(round_payload.get('base_text_kind', 'source'))})",
        )
        lines.append(f"- Profiles tried: `{', '.join(round_payload['profiles'])}`")
        lines.append(f"- Incoming failure tags: `{', '.join(round_payload['failure_tags_in']) or 'none'}`")
        if round_payload["strategy_directives"]:
            lines.append("- Strategy directives:")
            lines.extend(f"  - {item}" for item in round_payload["strategy_directives"])
        lines.append(f"- Decision: `{round_payload['decision']}`")
        lines.append(f"- Reason: {round_payload['reason']}")
        if round_payload.get("quality_gate_tags"):
            lines.append(
                f"- Quality gate: blocked by `{', '.join(round_payload['quality_gate_tags'])}`",
            )
        else:
            lines.append("- Quality gate: passed")
        lines.append(f"- Next step: `{round_payload.get('next_step', 'stop')}`")
        lines.append(f"- Delta: `{round_payload['delta']}`")
        lines.append("")
        lines.append("| Candidate | Profile | Final | Rules | Hard Fail | Failure Tags |")
        lines.append("| --- | --- | ---: | ---: | --- | --- |")
        for candidate in round_payload["candidates"]:
            score = candidate.get("score") or {}
            lines.append(
                "| "
                f"{candidate['candidate_index']} | "
                f"{candidate['profile']} | "
                f"{float(score.get('final_score') or 0.0):.6f} | "
                f"{float(score.get('rule_score') or 0.0):.6f} | "
                f"{bool(score.get('hard_fail', False))} | "
                f"{', '.join(candidate.get('failure_tags') or []) or 'none'} |",
            )
        selected = round_payload["selected_candidate"]
        lines.append("")
        lines.append(f"### Selected Candidate: {selected['profile']}")
        lines.append("")
        lines.append("```text")
        lines.append(selected["text"])
        lines.append("```")
        lines.append("")
    return "\n".join(lines)


def localize_decision(decision: str) -> str:
    mapping = {
        "keep": "保留 challenger",
        "continue": "继续优化，进入下一轮",
        "discard": "丢弃 challenger，保留 baseline",
    }
    return mapping.get(decision, decision)


def localize_winner(winner: str) -> str:
    mapping = {
        "baseline": "基线版本",
        "challenger": "挑战版本",
    }
    return mapping.get(winner, winner)


def localize_reason(reason: str) -> str:
    mapping = {
        "challenger improved beyond threshold": "challenger 超过阈值并明显更优",
        "selected challenger did not improve enough": "选中的 challenger 提升不足，未达到保留阈值",
        "selected challenger improved and passed quality gate": "选中的 challenger 提升明显，且已经通过结果质量门",
        "selected challenger improved, but quality gate requested another round": "选中的 challenger 虽然提升明显，但还残留结构性问题，所以继续下一轮修复",
        "selected challenger improved, but round budget was exhausted": "选中的 challenger 已经提升，但轮次预算已用完，只能保留当前最优版本",
        "selected challenger did not improve, but retryable quality issues remain": "选中的 challenger 没有提升，但仍有可重试的结构性问题，所以继续下一轮修复",
        "improvement below threshold": "提升幅度低于阈值",
        "challenger failed hard constraints": "challenger 未通过硬约束",
    }
    return mapping.get(reason, reason)


def localize_profile(profile: str) -> str:
    mapping = {
        "repair": "修复",
        "steady": "稳健",
        "reassuring": "安抚",
        "natural": "自然",
        "direct": "直接",
        "heuristic-natural": "启发式-自然",
        "heuristic-balanced": "启发式-平衡",
    }
    return mapping.get(profile, profile)


def localize_revision_mode(mode: str) -> str:
    mapping = {
        "rewrite": "从原文重写",
        "repair": "基于上一轮最佳版本继续修复",
    }
    return mapping.get(mode, mode)


def localize_base_text_kind(kind: str) -> str:
    mapping = {
        "source": "原文",
        "best_so_far": "上一轮最佳版本",
    }
    return mapping.get(kind, kind)


def localize_failure_tag(tag: str) -> str:
    mapping = {
        "template_tone": "模板腔",
        "hard_fail": "硬约束失败",
        "copied_baseline": "直接复用 baseline",
        "generation_error": "生成失败",
        "no_improvement": "没有提升",
        "wrong_audience": "对象错误",
        "too_short": "过短",
        "too_similar": "改写不明显",
        "source_template_carryover": "还保留了原文套话",
        "bad_splice": "句子拼接有问题",
        "placeholder_output": "像占位文本",
        "overcompressed": "压缩过头了",
        "regressed_from_best": "相对当前最优稿回退了",
        "too_vague": "过于空泛",
        "missing_must_include": "缺少必含信息",
        "none": "无",
    }
    return mapping.get(tag, tag)


def localize_failure_tags(tags: list[str]) -> str:
    if not tags:
        return "无"
    return "、".join(localize_failure_tag(tag) for tag in tags)


def localize_note(note: str) -> str:
    mapping = {
        "generation error: recovered candidate was too short": "生成失败：回收得到的候选文本过短",
        "generation error: recovered candidate did not satisfy hard constraints": "生成失败：回收得到的候选文本未满足硬约束",
        "generation error: recovered placeholder candidate": "生成失败：回收得到的是占位候选文本",
        "generation error: candidate recovery failed": "生成失败：候选文本恢复失败",
        "severe template carryover from source": "严重沿用了原文里的模板腔",
        "rewrite change is too small": "改写幅度太小，和原文仍然过近",
        "sentence structure is broken by phrase collision": "句子结构被重复尾句或连接语拼坏了",
        "candidate still looks like placeholder text": "候选文本看起来还是占位式内容",
        "rewrite removed too much source content": "改写时删掉了过多原文有效内容",
        "email reply could use clearer business context": "邮件正文的业务上下文还可以更明确",
        "too short for an email reply": "作为邮件回复偏短",
        "too short and vague for a complete reply": "作为完整文案偏短、偏泛",
        "could use clearer action or time detail": "动作或时间信息还可以更明确",
        "missing email-style greeting": "缺少邮件式称呼",
        "reads like a chat reply, not a full email body": "更像聊天回复，不像完整邮件正文",
        "email reply lacks clear body structure": "邮件正文结构不够清晰",
    }
    if note in mapping:
        return mapping[note]
    if note.startswith("contains template phrases: "):
        return "包含模板短语：" + note.split(": ", 1)[1]
    if note.startswith("retains source template phrases: "):
        return "保留了原文模板短语：" + note.split(": ", 1)[1]
    if note.startswith("rewrite too similar to source"):
        return "改写和原文过于相似"
    if note.startswith("rewrite still very close to source"):
        return "改写仍然和原文非常接近"
    if note.startswith("rewrite remains close to source"):
        return "改写和原文仍然偏近"
    if note.startswith("rewrite drops too much source detail"):
        return "改写删掉了太多原文细节"
    if note.startswith("rewrite is over-compressed for the source length"):
        return "相对原文长度来说，改写压缩得过头了"
    if note.startswith("rewrite is quite compressed compared with the source"):
        return "改写相对原文压缩得偏多"
    if note.startswith("contains placeholder-style ellipsis"):
        return "出现了省略号式占位表达"
    if note.startswith("contains placeholder-style content"):
        return "出现了占位式内容"
    if note.startswith("sentence splice issue: repeated lead-in connectors"):
        return "句子里重复出现引导连接语，读起来像拼接"
    if note.startswith("sentence splice issue: collided closing phrases"):
        return "一句话里叠了两个结尾短语，读起来像硬拼"
    if note.startswith("candidate reintroduces more template phrases than current best"):
        return "这个候选把当前最优稿已经压下去的模板短语又带回来了"
    if note.startswith("candidate drifts back toward source wording"):
        return "这个候选的措辞又往原文模板方向回去了"
    if note.startswith("candidate compresses current best too aggressively"):
        return "这个候选把当前最优稿压缩得太狠了"
    if note.startswith("candidate collapses paragraph structure compared with current best"):
        return "这个候选把当前最优稿的段落结构压塌了"
    return note


def build_user_visible_summary(
    *,
    task: str,
    compare_payload: dict[str, Any],
    baseline_text: str,
    challenger_text: str,
    session_trace: list[dict[str, Any]],
    report_html_path: str,
    trace_path: str,
    run_budget: dict[str, Any] | None = None,
) -> str:
    lines: list[str] = []
    lines.append("# Humanize 优化摘要")
    lines.append("")
    lines.append(f"- 任务：{task}")
    lines.append(
        f"- 最终决策：`{compare_payload['decision']}`（{localize_decision(compare_payload['decision'])}）",
    )
    lines.append(
        f"- 胜出版本：`{compare_payload['winner']}`（{localize_winner(compare_payload['winner'])}）",
    )
    lines.append(f"- 原因：{localize_reason(compare_payload['reason'])}")
    lines.append(f"- 分数变化：`{compare_payload['delta']}`")
    lines.append(f"- 保留阈值：`{compare_payload['margin']}`")
    if run_budget:
        lines.append(f"- 最大轮数：`{run_budget.get('max_rounds')}`（通过质量门会提前停止）")
    lines.append("")
    winner_text = challenger_text if compare_payload.get("winner") == "challenger" else baseline_text
    lines.append("## 最终结果")
    lines.append("")
    lines.append("```text")
    lines.append(winner_text)
    lines.append("```")
    lines.append("")
    lines.append("## 基线版本")
    lines.append("")
    lines.append("```text")
    lines.append(baseline_text)
    lines.append("```")
    lines.append("")
    lines.append("## 最终挑战版本")
    lines.append("")
    lines.append("```text")
    lines.append(challenger_text)
    lines.append("```")
    lines.append("")
    lines.append("## 优化过程")
    lines.append("")
    for round_payload in session_trace:
        candidate_profiles = []
        for candidate in round_payload["candidates"]:
            profile = candidate.get("profile")
            if profile and profile not in candidate_profiles:
                candidate_profiles.append(profile)
        lines.append(f"### 第 {round_payload['round']} 轮")
        lines.append("")
        lines.append(
            f"- 本轮开始时的 baseline 分数：`{round_payload['baseline_score']['final_score']}`",
        )
        lines.append(
            f"- 修订模式：`{round_payload.get('revision_mode', 'rewrite')}`（{localize_revision_mode(round_payload.get('revision_mode', 'rewrite'))}）",
        )
        lines.append(
            f"- 本轮修订来源：`{round_payload.get('base_text_kind', 'source')}`（{localize_base_text_kind(round_payload.get('base_text_kind', 'source'))}）",
        )
        lines.append(
            f"- 本轮决策：`{round_payload['decision']}`（{localize_decision(round_payload['decision'])}）",
        )
        lines.append(f"- 原因：{localize_reason(round_payload['reason'])}")
        if round_payload.get("quality_gate_tags"):
            lines.append(
                f"- 结果质量门：未通过（残留问题：{localize_failure_tags(round_payload['quality_gate_tags'])}）",
            )
        else:
            lines.append("- 结果质量门：通过")
        next_step = str(round_payload.get("next_step") or "stop")
        next_step_text = "进入下一轮继续修复" if next_step == "continue" else "在本轮停止"
        lines.append(f"- 下一步：{next_step_text}")
        lines.append(f"- 分数变化：`{round_payload['delta']}`")
        lines.append(
            f"- 尝试的 profiles：`{'、'.join(localize_profile(profile) for profile in candidate_profiles)}`",
        )
        lines.append(
            f"- 继承的失败标签：`{localize_failure_tags(round_payload['failure_tags_in'])}`",
        )
        lines.append("")
        lines.append("| 候选 | Profile | 总分 | 规则分 | 硬失败 | 失败标签 |")
        lines.append("| --- | --- | ---: | ---: | --- | --- |")
        for candidate in round_payload["candidates"]:
            score = candidate.get("score") or {}
            lines.append(
                "| "
                f"{candidate['candidate_index']} | "
                f"{localize_profile(candidate['profile'])} | "
                f"{float(score.get('final_score') or 0.0):.6f} | "
                f"{float(score.get('rule_score') or 0.0):.6f} | "
                f"{bool(score.get('hard_fail', False))} | "
                f"{localize_failure_tags(candidate.get('failure_tags') or [])} |",
            )
        lines.append("")
        for candidate in round_payload["candidates"]:
            candidate_score = candidate.get("score") or {}
            lines.append(
                f"候选 {candidate['candidate_index']} · `{localize_profile(candidate['profile'])}`",
            )
            lines.append("")
            candidate_text = candidate.get("text") or ""
            if candidate_text.strip():
                lines.append("```text")
                lines.append(candidate_text)
                lines.append("```")
            else:
                lines.append("_该候选没有产出可用文本。_")
            if candidate.get("failure_tags"):
                lines.append("")
                lines.append("失败标签：" + localize_failure_tags(candidate["failure_tags"]))
            if (candidate_score.get("notes") or []):
                lines.append("")
                lines.append("备注：")
                lines.extend(f"- {localize_note(compact_note(note))}" for note in candidate_score["notes"])
            lines.append("")
        selected = round_payload["selected_candidate"]
        lines.append(
            "本轮选中候选："
            f"`{localize_profile(selected['profile'])}`（候选 {selected['candidate_index']}）"
        )
        lines.append("")
    lines.append("## 文件")
    lines.append("")
    lines.append(f"- 完整报告：`{report_html_path}`")
    lines.append(f"- 完整追踪 JSON：`{trace_path}`")
    lines.append("")
    return "\n".join(lines)


def compact_note(note: str) -> str:
    normalized = " ".join(str(note).split())
    if normalized.startswith("generation error:"):
        lowered = normalized.lower()
        if "too short" in lowered:
            return "generation error: recovered candidate was too short"
        if "hard constraints" in lowered:
            return "generation error: recovered candidate did not satisfy hard constraints"
        if "placeholder candidate" in lowered:
            return "generation error: recovered placeholder candidate"
        return "generation error: candidate recovery failed"
    if len(normalized) > 180:
        return normalized[:177] + "..."
    return normalized


def build_user_visible_html(markdown_text: str) -> str:
    escaped = (
        markdown_text.replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
    )
    return f"""<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Humanize 优化摘要</title>
  <style>
    body {{
      margin: 0;
      padding: 28px;
      background: #f5f1e8;
      color: #1a1a17;
      font: 15px/1.6 "Iowan Old Style", "Palatino Linotype", serif;
    }}
    .shell {{
      max-width: 1100px;
      margin: 0 auto;
      background: rgba(255,250,240,0.94);
      border: 1px solid #d9d1c0;
      border-radius: 18px;
      box-shadow: 0 14px 34px rgba(24, 28, 34, 0.08);
      padding: 24px;
    }}
    pre {{
      white-space: pre-wrap;
      word-break: break-word;
      margin: 0;
      font: 14px/1.65 ui-monospace, SFMono-Regular, Menlo, monospace;
    }}
  </style>
</head>
<body>
  <main class="shell">
    <pre>{escaped}</pre>
  </main>
</body>
</html>
"""


def compare_payload_local(
    *,
    spec_path: Path,
    source_path: Path,
    baseline_path: Path,
    challenger_path: Path,
    baseline_score: dict[str, Any],
    challenger_score: dict[str, Any],
    margin: float,
) -> dict[str, Any]:
    delta = float(challenger_score.get("final_score") or 0.0) - float(baseline_score.get("final_score") or 0.0)
    if challenger_score.get("hard_fail"):
        decision = "discard"
        winner = "baseline"
        reason = "challenger failed hard constraints"
    elif delta >= margin:
        decision = "keep"
        winner = "challenger"
        reason = "challenger improved beyond threshold"
    else:
        decision = "discard"
        winner = "baseline"
        reason = "improvement below threshold"

    return {
        "decision": decision,
        "winner": winner,
        "reason": reason,
        "margin": margin,
        "delta": round(delta, 6),
        "spec_path": str(spec_path.resolve()),
        "source_path": str(source_path.resolve()) if source_path.exists() else "",
        "baseline": {
            "path": str(baseline_path.resolve()),
            **baseline_score,
        },
        "challenger": {
            "path": str(challenger_path.resolve()),
            **challenger_score,
        },
    }


def write_round_log_local(run_dir: Path, compare_payload: dict[str, Any]) -> None:
    payload = dict(compare_payload)
    payload["recorded_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    log_path = run_dir / "rounds.jsonl"
    with log_path.open("a", encoding="utf-8") as fh:
        fh.write(json.dumps(payload, ensure_ascii=False) + "\n")

    rounds_dir = run_dir / "rounds"
    rounds_dir.mkdir(exist_ok=True)
    index = len(list(rounds_dir.glob("round-*.json"))) + 1
    (rounds_dir / f"round-{index:03d}.json").write_text(
        json.dumps(payload, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )


def recover_candidate_from_response(response: dict[str, Any], hard_constraints: dict[str, Any]) -> str:
    choices = response.get("choices") or []
    if not choices:
        return ""
    message = choices[0].get("message") or {}
    reasoning = str(message.get("reasoning_content") or "").strip()
    if not reasoning:
        return ""

    must_include = [str(item).strip() for item in hard_constraints.get("must_include") or [] if str(item).strip()]
    max_chars = hard_constraints.get("max_chars")

    def valid(text: str) -> bool:
        compact = text.replace(" ", "").replace("\n", "")
        if max_chars is not None and len(compact) > int(max_chars):
            return False
        if must_include and any(item not in text for item in must_include):
            return False
        return len(compact) >= 10

    quoted = [
        item.strip()
        for item in __import__("re").findall(r"[“\"「『]([^”\"」』\n]{8,140})[”\"」』]", reasoning)
        if item.strip()
    ]
    valid_quoted = [item for item in quoted if valid(item)]
    if valid_quoted:
        return max(valid_quoted, key=len)

    labeled = __import__("re").findall(
        r"(?:最终文案|最终回复|最终候选|候选文案|草稿|建议文案)\s*[：:]\s*([^\n]{8,160})",
        reasoning,
    )
    valid_labeled = [item.strip(" “\"'”") for item in labeled if valid(item.strip(" “\"'”"))]
    if valid_labeled:
        return max(valid_labeled, key=len)

    return ""


def is_placeholder_candidate(text: str) -> bool:
    stripped = text.strip().strip("。.!！?？")
    if not stripped:
        return True
    if "<最终文案>" in stripped or "FINAL_CANDIDATE" in stripped:
        return True
    if stripped in {"最终文案", "候选文案", "<最终文案>"}:
        return True
    return False


def satisfies_hard_constraints(text: str, hard_constraints: dict[str, Any]) -> bool:
    max_chars = hard_constraints.get("max_chars")
    must_include = [str(item).strip() for item in hard_constraints.get("must_include") or [] if str(item).strip()]
    compact = text.replace(" ", "").replace("\n", "")
    if max_chars is not None and len(compact) > int(max_chars):
        return False
    if must_include and any(item not in text for item in must_include):
        return False
    return True


def infer_progress_context(task: str) -> tuple[str, str]:
    issue = ""
    action = ""
    issue_match = re.search(r"卡在([^，。；;]+)", task)
    if issue_match:
        issue = issue_match.group(1).strip()
    action_match = re.search(r"(今天(?:已经|已)?[^，。；;]*(?:处理|跟进|确认|沟通))", task)
    if action_match:
        action = action_match.group(1).strip()
    return issue, action


def is_social_decline_task(task: str) -> bool:
    return (
        any(marker in task for marker in ("不要太冷淡", "别太冷淡", "婉拒", "聚餐", "约饭", "邀约"))
        or (
            "朋友" in task
            and "朋友圈" not in task
            and any(marker in task for marker in ("拒绝", "不去", "去不了", "今晚", "聚餐", "约饭", "邀约"))
        )
    )


COPY_TASK_MARKERS = (
    "差评",
    "评价",
    "评论",
    "公告",
    "社群",
    "自媒体",
    "小红书",
    "朋友圈",
    "文案",
    "口播",
    "种草",
    "招聘",
    "岗位",
    "JD",
    "jd",
    "商品详情",
    "电商",
    "直播",
    "课程",
    "退款",
    "客服",
    "售后",
    "App",
    "APP",
    "推送",
)


def build_fallback_baseline(task: str, hard_constraints: dict[str, Any]) -> str:
    must_include = [str(item).strip() for item in hard_constraints.get("must_include") or [] if str(item).strip()]
    max_chars = hard_constraints.get("max_chars")
    is_email = "邮件" in task or "email" in task.lower()
    time_term = infer_time_term(must_include, task)
    deadline = deadline_term(time_term)
    other_terms = [item for item in must_include if item != time_term]
    joined_terms = "、".join(other_terms)
    primary_term = other_terms[0] if other_terms else ""
    tail_terms = "、".join(other_terms[1:]) if len(other_terms) > 1 else ""
    is_social_decline = is_social_decline_task(task)
    is_service = any(marker in task for marker in ("售后", "投诉", "安抚", "破损", "退款", "退换"))
    is_app_push = any(marker in task for marker in ("App", "APP", "推送", "直播课")) and any(
        marker in task for marker in ("直播", "课程", "课")
    )
    is_repair_request = any(marker in task for marker in ("房东", "维修", "报修", "空调", "不制冷"))
    is_peer_sync = (
        any(marker in task for marker in ("同事", "飞书", "测试环境", "接口字段"))
        and any(marker in task for marker in ("同步", "调整", "变更"))
    )
    issue, action = infer_progress_context(task)

    if is_app_push:
        candidate = f"{time_term}直播课开始，记得来听。"
    elif is_email and any(marker in task for marker in ("合作方", "合同附件", "催资料", "项目排期")):
        candidate = f"您好，关于{primary_term or '合同编号'}的相关附件还没收到，会影响项目排期，麻烦{deadline}补一下，谢谢。"
    elif is_email and "客户" in task:
        candidate = (
            f"您好，\n\n目前这件事还在推进中，我们会围绕{joined_terms or '相关事项'}继续确认，"
            f"预计{time_term}给您更明确的回复。\n\n谢谢您的耐心等待。"
        )
    elif is_email:
        candidate = (
            f"您好，\n\n目前这件事还在推进中，我们会围绕{joined_terms or '相关事项'}继续确认，"
            f"预计{time_term}向您同步更明确的进展。\n\n谢谢。"
        )
    elif any(marker in task for marker in ("老板", "上级", "领导")):
        if issue or action:
            candidate = f"老板，{issue or joined_terms or '这件事'}{action or '今天已经跟进'}，{time_term}同步。"
        else:
            candidate = (
                f"老板，今天和{primary_term or '相关方'}已经沟通过了，{time_term}我把"
                f"{tail_terms or '进展'}整理好发您。"
            )
    elif is_social_decline:
        candidate = f"{time_term}不去了，今天要赶项目，下次吧。"
    elif is_repair_request:
        issue_text = "空调不制冷" if "空调" in task and "不制冷" in task else (primary_term or "这边")
        candidate = f"您好，{issue_text}，想约{time_term}维修，您看方便安排吗？"
    elif is_peer_sync:
        candidate = f"接口字段今天有调整，麻烦{deadline}同步到测试环境，谢谢。"
    elif is_service:
        candidate = f"您好，{joined_terms or '这件事'}这边在跟进，{time_term}给您回复。"
    elif "客户" in task:
        candidate = f"您好，{joined_terms or '这件事'}这边还在跟进，预计{time_term}给您明确回复。"
    elif len(must_include) >= 2:
        candidate = f"目前正在围绕{joined_terms or '相关事项'}继续确认，预计{time_term}给您明确回复。"
    elif len(must_include) == 1:
        candidate = f"目前正在推进中，预计{time_term}给您明确回复。"
    else:
        candidate = "您好，这件事我正在跟进，稍后给您同步最新进展。"

    compact = candidate.replace(" ", "").replace("\n", "")
    if max_chars is not None and len(compact) > int(max_chars):
        candidate = candidate[: int(max_chars)].rstrip("，。； ") + "。"
    return candidate


def call_and_extract_candidate(
    *,
    base_url: str,
    model: str,
    system_prompt: str,
    user_prompt: str,
    temperature: float,
    max_tokens_sequence: list[int],
    hard_constraints: dict[str, Any],
    require_hard_constraints: bool,
    min_chars_hint: int,
) -> tuple[str, dict[str, Any]]:
    last_exc: Exception | None = None
    last_response: dict[str, Any] | None = None
    for index, max_tokens in enumerate(max_tokens_sequence):
        prompt = user_prompt
        if index > 0:
            prompt += "\n补充要求：不要继续展开分析，直接在最后给出 FINAL_CANDIDATE: <最终文案>。"
        response = call_chat(
            base_url=base_url,
            model=model,
            system_prompt=system_prompt,
            user_prompt=prompt,
            temperature=temperature,
            max_tokens=max_tokens,
        )
        last_response = response
        try:
            candidate = extract_content(response)
        except Exception as exc:
            recovered = recover_candidate_from_response(response, hard_constraints)
            if recovered:
                candidate = recovered
            else:
                last_exc = exc
                continue
        if is_placeholder_candidate(candidate):
            last_exc = RuntimeError(f"Recovered placeholder candidate: {candidate}")
            continue
        if require_hard_constraints and not satisfies_hard_constraints(candidate, hard_constraints):
            last_exc = RuntimeError(f"Recovered candidate does not satisfy hard constraints: {candidate}")
            continue
        compact = candidate.replace(" ", "").replace("\n", "")
        if min_chars_hint > 0 and len(compact) < min_chars_hint:
            last_exc = RuntimeError(f"Recovered candidate is too short: {candidate}")
            continue
        return candidate, response
    raise RuntimeError(
        "Failed to recover a final candidate from local generation"
        + (f": {last_exc}" if last_exc else "")
        + (f". Last response: {last_response}" if last_response else ""),
    )


def generate_candidate(
    *,
    run_dir: Path,
    spec: dict[str, Any],
    session_mode: str,
    task: str,
    hard_constraints: dict[str, Any],
    source_text: str,
    current_best_text: str,
    base_url: str,
    model: str,
    profile: str,
    strategy_directives: list[str],
    failure_tags: list[str],
    revision_mode: str,
    round_number: int,
    candidate_index: int,
    current_best_score_value: float,
    enforce_continuity: bool,
) -> dict[str, Any]:
    (_, _), (challenger_system, challenger_user) = build_generation_prompts(
        task=task,
        hard_constraints=hard_constraints,
        original=source_text if session_mode == "rewrite" else "",
        mode=session_mode,
        challenger_profile=profile,
        strategy_directives=strategy_directives,
        failure_tags=failure_tags,
        revision_mode=revision_mode,
    )
    candidate_text, response = call_and_extract_candidate(
        base_url=base_url,
        model=model,
        system_prompt=challenger_system,
        user_prompt=challenger_user.format(baseline=current_best_text),
        temperature=0.35,
        max_tokens_sequence=[220, 320, 420],
        hard_constraints=hard_constraints,
        require_hard_constraints=False,
        min_chars_hint=32 if ("邮件" in task or "email" in task.lower()) else (16 if "客户" in task else 12),
    )

    stem = f"round-{round_number:03d}-candidate-{candidate_index:02d}"
    generation_path = run_dir / "rounds" / f"{stem}.generation.json"
    candidate_path = run_dir / "rounds" / f"{stem}.txt"
    score_path = run_dir / "rounds" / f"{stem}.score.json"

    write_json(generation_path, response)
    write_text(candidate_path, candidate_text)

    score_obj = score_candidate(
        spec,
        candidate_text,
        source_text,
    )
    score_payload = score_obj.as_dict()
    failure_tags_out = extract_failure_tags(
        task,
        candidate_text,
        score_payload,
        baseline_score=current_best_score_value,
    )
    if candidate_text.strip() == current_best_text.strip() and "copied_baseline" not in failure_tags_out:
        failure_tags_out.append("copied_baseline")
    score_payload, failure_tags_out = apply_best_so_far_guardrails(
        candidate_text=candidate_text,
        current_best_text=current_best_text,
        source_text=source_text,
        score_payload=score_payload,
        failure_tags_out=failure_tags_out,
        enforce_continuity=enforce_continuity,
    )
    write_json(
        score_path,
        {
            "candidate_path": str(candidate_path.resolve()),
            "generation_path": str(generation_path.resolve()),
            **score_payload,
        },
    )

    return {
        "candidate_index": candidate_index,
        "profile": profile,
        "text": candidate_text,
        "candidate_path": str(candidate_path.resolve()),
        "generation_path": str(generation_path.resolve()),
        "score_path": str(score_path.resolve()),
        "score": score_payload,
        "failure_tags": failure_tags_out,
    }


def infer_time_phrase(text: str) -> str:
    match = re.search(r"周[一二三四五六日天](?:早上|上午|中午|下午|晚上)?", text)
    if match:
        return match.group(0)
    for phrase in ["明天下午", "明天上午", "今天下午", "今天上午", "明早", "明晚", "今早", "今晚", "今天", "本周内", "稍后"]:
        if phrase in text:
            return phrase
    return "稍后"


def infer_time_term(must_include: list[str], source_text: str) -> str:
    for item in must_include:
        if any(token in item for token in ("今天", "明天", "明早", "明晚", "今早", "今晚", "本周", "稍后")):
            return item
    return infer_time_phrase(source_text)


def deadline_term(time_term: str) -> str:
    if time_term.endswith(("前", "内")):
        return time_term
    return f"{time_term}前"


def looks_like_short_message_task(task: str, hard_constraints: dict[str, Any] | None = None) -> bool:
    lowered = task.lower()
    if "邮件" in task or "email" in lowered:
        return False
    markers = (
        "客户",
        "微信",
        "飞书",
        "同事",
        "回复",
        "消息",
        "沟通",
        "老板",
        "上级",
        "领导",
        "聚餐",
        "约饭",
        "邀约",
        "婉拒",
        "拒绝",
        "售后",
        "投诉",
        "安抚",
        "App",
        "APP",
        "推送",
        "直播课",
    )
    if any(marker in task for marker in markers) or is_social_decline_task(task):
        return True
    max_chars = (hard_constraints or {}).get("max_chars")
    return bool(max_chars and int(max_chars) <= 120 and len(task) <= 80)


def infer_service_issue(task: str, source_text: str) -> str:
    context = f"{task}。{source_text}"
    patterns = [
        r"申请([^，。；;]{1,24})的客户",
        r"反馈的([^，。；;]{1,32})",
        r"咨询的([^，。；;]{1,32})",
        r"关于您([^，。；;]{1,32})",
    ]
    for pattern in patterns:
        match = re.search(pattern, context)
        if not match:
            continue
        issue = match.group(1).strip()
        issue = re.sub(r"(诉求|事项|问题|情况)$", "", issue).strip()
        issue = re.sub(r"^(当前|这个|相关)", "", issue).strip()
        if issue and len(issue) <= 24:
            return f"{issue}问题" if not issue.endswith("问题") else issue
    service_keywords = (
        "退款",
        "退换",
        "破损",
        "延迟",
        "配送",
        "物流",
        "投诉",
        "售后",
        "发票",
        "订单",
    )
    for keyword in service_keywords:
        if keyword in context:
            return f"{keyword}问题"
    return "这个问题"


def looks_like_professional_email(task: str, source_text: str) -> bool:
    lowered_task = task.lower()
    if ("微信" in task or "wechat" in lowered_task or "消息" in task) and not ("邮件" in task or "email" in lowered_task):
        return False
    if any(marker in task for marker in COPY_TASK_MARKERS) and not ("邮件" in task or "email" in lowered_task):
        return False
    if "邮件" in task or "email" in lowered_task:
        return True
    markers = (
        "尊敬的",
        "贵公司",
        "进一步通知",
        "岗位职责",
        "有序处理中",
        "竭诚为您服务",
        "感谢您的时间与安排",
        "如您后续还需要我补充",
    )
    return any(marker in source_text for marker in markers)


def _normalize_compare_text(text: str) -> str:
    compact = re.sub(r"\s+", "", text)
    return re.sub(r"[，。！？；：、,.!?:;\"'“”‘’（）()《》【】\\[\\]<>·—…-]", "", compact)


def _similarity_ratio(left: str, right: str) -> float:
    left_norm = _normalize_compare_text(left)
    right_norm = _normalize_compare_text(right)
    if not left_norm or not right_norm:
        return 0.0
    return SequenceMatcher(None, left_norm, right_norm).ratio()


def _template_phrase_hits(text: str) -> list[str]:
    return [phrase for phrase in DEFAULT_TEMPLATE_PHRASES if phrase in text]


def _compact_len(text: str) -> int:
    return len(re.sub(r"\s+", "", text))


def _generic_template_repair_replacements(*, natural: bool) -> list[tuple[str, str]]:
    replacements = [
        (r"成长这件事的底层逻辑", "成长这件事到底该怎么想"),
        (r"底层逻辑", "更核心的问题" if not natural else "根子上的问题"),
        (r"认知升级系统", "更适合自己的思考方式"),
        (r"行动迭代机制", "行动节奏"),
        (r"成长闭环", "一套能持续转起来的方法" if not natural else "一套能转起来的方法"),
        (r"低质量勤奋", "表面上的忙" if not natural else "瞎忙"),
        (r"系统化交流", "一起展开交流" if not natural else "一起好好聊聊"),
        (r"高质量学习型社群", "更愿意认真交流的社群" if not natural else "更愿意认真交流的群"),
        (r"系统化理解", "真正理解"),
        (r"高价值直播分享", "直播分享" if not natural else "直播"),
        (r"可持续增长", "更稳的增长"),
        (r"结构化表达", "更有条理地表达"),
        (r"影响力输出能力", "把重点说清楚的能力"),
        (r"个人竞争力跃迁", "个人状态往前走一截"),
        (r"释放增长潜能", "把增长往前推一推"),
        (r"深度拆解", "尽量讲具体"),
        (r"全面提升", "慢慢提上来"),
        (r"数字化浪潮", "现在这波数字化变化"),
        (r"数字化转型", "数字化升级"),
        (r"多维度提升", "一起往前提"),
        (r"组织协同体系", "团队协作方式"),
        (r"感谢您的理解与支持", "谢谢您的理解"),
        (r"感谢您的耐心等待", "感谢您的耐心"),
        (r"已经高度重视并同步相关部门进行综合核实", "已经在和相关同事核实"),
        (r"高度重视并同步相关部门进行综合核实", "在和相关同事核实"),
        (r"正在有序推进中", "还在推进中"),
        (r"为您提供完整处理结果", "把处理结果同步给您"),
        (r"持续为您提供优质服务", "继续跟进这件事"),
        (r"欢迎随时与我们联系", "欢迎随时联系我"),
        (r"我们将竭诚为您服务", "我会继续跟进"),
    ]
    return replacements


def _apply_generic_template_repairs(text: str, *, natural: bool) -> str:
    return _apply_replacements(text, _generic_template_repair_replacements(natural=natural))


def rewrite_professional_email(
    source_text: str,
    *,
    natural: bool,
    repair: bool = False,
    source_reference: str = "",
    failure_tags: list[str] | None = None,
) -> str:
    text = _compact_copy(source_text)
    failure_tags = failure_tags or []
    replacements: list[tuple[str, str]] = [
        (r"^尊敬的[^，。]*，您好。?", "您好。"),
        (r"尊敬的客户，您好。?", "您好。"),
        (r"尊敬的招聘负责人，您好。?", "您好。"),
        (r"您好，冒昧打扰。?", "您好，打扰了。"),
        (r"非常感谢贵公司日前为我提供本次面试机会。", "感谢您安排这次面试。"),
        (r"非常感谢贵司此前给予我的面试机会。?", "感谢您前面给我的面试机会。"),
        (r"我想就此前参与的([^。]*?)与您进行一次礼貌性的跟进确认", r"想跟您跟进一下此前参与的\1"),
        (r"我想就([^。]*?)与您进行一次礼貌性的跟进确认", r"想跟您跟进一下\1"),
        (r"通过本次沟通，我对([^。]+)有了更加全面且深入的理解，也进一步增强了我希望加入贵公司的意愿。", r"这次沟通后，我对\1有了更具体的了解，也更希望有机会加入贵公司。"),
        (r"结合本次面试交流内容，我更加确信自身过往经历与该岗位需求之间具有较高的匹配度，也期待后续能够有机会继续推进相关流程。", "这次交流也让我更确认，自己的经历和岗位需求比较匹配，也期待后续能继续推进。"),
        (r"面试结束后，我对该岗位的理解进一步加深，也更加期待能够加入团队贡献自己的价值。?", "面试结束后，我对这个岗位的理解更具体了一些，也更希望有机会加入团队。"),
        (r"冒昧来信是想礼貌性跟进一下当前招聘流程的最新进展，?", "这封邮件主要是想跟进一下目前的招聘进度。"),
        (r"若后续还需要我补充任何信息或材料，我都会第一时间配合提供。?", "如果后面还需要我补充材料或信息，我也可以马上配合。"),
        (r"感谢您的时间与关注，期待您的回复。?", "谢谢您，期待您的回复。"),
        (r"针对您当前咨询的([^。]*?)事项", r"关于您咨询的\1"),
        (r"关于您当前咨询的([^。]*?)事项", r"关于您咨询的\1"),
        (r"我方已经同步协调", "我们已经和"),
        (r"我司已第一时间协调", "这边已经联系"),
        (r"相关财务同事进行进一步核实与推进", "财务同事确认并继续跟进"),
        (r"相关同事进行进一步核实与推进", "相关同事确认并继续跟进"),
        (r"相关部门进行核查处理", "相关同事核实处理"),
        (r"礼貌性的跟进确认", "跟进一下"),
        (r"进一步核实与推进", "继续确认和跟进"),
        (r"现阶段整体流程正在有序处理中", "目前这件事还在处理中"),
        (r"目前物流链路正在持续推进中", "目前物流这边还在跟进"),
        (r"预计会在([^，。]+)为您提供更加清晰和完整的反馈说明", r"预计\1给您更明确的回复"),
        (r"预计将在24小时内同步最新进展", "预计24小时内给您同步最新进展"),
        (r"如目前流程仍在推进中，我将持续保持关注，并期待后续有进一步消息。?", "如果流程还在推进，也想麻烦您有消息时告诉我一声。"),
        (r"如您后续还需要我补充任何材料、信息或案例，欢迎随时与我联系，我将第一时间积极配合。", "如果后续还需要我补充材料、信息或案例，欢迎随时联系我，我会第一时间配合。"),
        (r"再次感谢您的时间与安排，期待您的进一步通知。?", "再次感谢您的时间和安排，期待您的回复。"),
        (r"感谢您的耐心等待与理解支持", "感谢您的耐心"),
        (r"感谢您在百忙之中查阅此邮件", "感谢您抽空看这封邮件"),
        (r"感谢您的理解与支持", "谢谢您的理解"),
        (r"欢迎随时与我们联系，我们将竭诚为您服务。?", "如有需要，欢迎随时联系我。"),
        (r"如后续您有任何疑问，欢迎随时与我们联系，我?们?将竭诚为您服务。?", "如有其他问题，欢迎随时联系我。"),
        (r"我们注意到贵公司当前正处于数字化转型和业务增长的关键阶段，因此希望向您推荐我司全新升级的一站式智能营销解决方案。?", "看到贵公司最近可能也在关注增长和数字化这块，所以想跟您简单介绍一下我们的智能营销方案。"),
        (r"该方案能够从多维度提升线索获取效率、客户触达质量以及转化链路表现，帮助企业在复杂多变的市场环境中构建更加稳健和可持续的增长体系。?", "它主要是帮团队把线索获取、客户触达和转化跟进这几件事做得更顺一点。"),
        (r"如您方便，我们期待与您安排一次简短沟通，以便进一步介绍相关能力与合作价值。?", "如果您方便，我想约个十几分钟，简单看看这块对您现在的业务有没有帮助。"),
    ]
    if natural:
        replacements.extend(
            [
                (r"您好。", "您好，"),
                (r"这次沟通后，我对([^。]+)有了更具体的了解，也更希望有机会加入贵公司。", r"和您沟通之后，我对\1的理解更具体了，也更希望有机会加入贵公司。"),
                (r"关于您咨询的([^。]+)，我们已经和", r"关于您咨询的\1，这边我们已经和"),
                (r"想跟您简单介绍一下", "想先跟您简单聊一下"),
            ],
        )
    text = _apply_replacements(text, replacements)
    if repair:
        if {"template_tone", "source_template_carryover", "too_similar"} & set(failure_tags):
            text = _apply_generic_template_repairs(text, natural=natural)
        if "bad_splice" in failure_tags:
            text = cleanup_common_phrase_collisions(text)
        if "overcompressed" in failure_tags and source_reference:
            source_reference = _compact_copy(source_reference)
            if _compact_len(text) < _compact_len(source_reference) * 0.72:
                source_sentences = [part.strip() for part in re.split(r"[。！？]", source_reference) if part.strip()]
                text_sentences = [part.strip() for part in re.split(r"[。！？]", text) if part.strip()]
                for sentence in source_sentences:
                    if sentence and all(_similarity_ratio(sentence, existing) < 0.72 for existing in text_sentences):
                        text = text.rstrip("。") + "。\n\n" + sentence + "。"
                        break
    text = re.sub(r"([。！？])(?=[^\n])", r"\1\n\n", text)
    text = re.sub(r"\n{3,}", "\n\n", text).strip()
    return text


def build_email_rewrite_heuristics(
    task: str,
    source_text: str,
    current_best_text: str,
    failure_tags: list[str],
    *,
    repair: bool,
) -> list[tuple[str, str]]:
    if not looks_like_professional_email(task, source_text):
        return []
    base_text = current_best_text if repair else source_text
    return [
        (
            "heuristic-natural",
            rewrite_professional_email(
                base_text,
                natural=True,
                repair=repair,
                source_reference=source_text,
                failure_tags=failure_tags,
            ),
        ),
        (
            "heuristic-balanced",
            rewrite_professional_email(
                base_text,
                natural=False,
                repair=repair,
                source_reference=source_text,
                failure_tags=failure_tags,
            ),
        ),
    ]


def _compact_copy(text: str) -> str:
    text = text.replace("\r\n", "\n")
    text = re.sub(r"[ \t]+", "", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()


def looks_like_longform_rewrite(task: str, source_text: str) -> bool:
    compact = re.sub(r"\s+", "", source_text)
    is_copy_task = any(marker in task for marker in COPY_TASK_MARKERS) or "更像真人" in task
    ai_copy_markers = (
        "底层逻辑",
        "认知升级",
        "行动迭代",
        "成长闭环",
        "高效修护体验",
        "多重植萃",
        "肌肤底层",
        "高度自驱力",
        "优秀沟通协作能力",
        "复杂问题拆解能力",
        "业务需求洞察",
        "产品方案设计",
        "跨部门资源协调",
        "项目落地推进",
        "高品质亲肤面料",
        "人体工学剪裁",
        "时尚表现力",
        "系统化理解",
        "内容增长方法论",
        "高价值直播",
        "转化路径",
        "可持续增长",
        "系统化表达训练",
        "全面提升",
        "竞争力跃迁",
        "退款诉求",
        "规定时效",
        "相关部门进行核实处理",
    )
    source_looks_template = any(marker in source_text for marker in ai_copy_markers)
    generic_longform_shape = len(compact) >= 90 and source_text.count("。") >= 1
    min_chars = 45 if is_copy_task else 80
    return len(compact) >= min_chars and (
        is_copy_task
        or source_looks_template
        or "评论区" in source_text
        or "大家" in source_text[:80]
        or generic_longform_shape
    )


def _apply_replacements(text: str, replacements: list[tuple[str, str]]) -> str:
    updated = text
    for pattern, repl in replacements:
        updated = re.sub(pattern, repl, updated)
    return updated


def rewrite_longform_copy(
    source_text: str,
    *,
    natural: bool,
    repair: bool = False,
    source_reference: str = "",
    failure_tags: list[str] | None = None,
) -> str:
    text = _compact_copy(source_text)
    failure_tags = failure_tags or []
    replacements: list[tuple[str, str]] = [
        (r"在当下这个充满不确定性却又蕴含巨大机会的时代", "这几年变化真的很快"),
        (r"每一个普通人都比以往任何时候更需要重新思考", "普通人真的得重新想想"),
        (r"很多人表面上看起来很努力，也投入了大量时间与精力，但最终却没有获得预期中的结果。", "很多人明明也很努力，时间精力都花了，但结果还是不理想。"),
        (r"为什么会这样？我认为，核心原因并不在于", "为什么会这样？问题往往不在于"),
        (r"有没有建立起一套真正适合自己的认知升级系统和行动迭代机制", "有没有形成一套适合自己的思考方式和行动节奏"),
        (r"一个人真正拉开差距的，从来都不是短期的爆发，而是长期稳定的自我优化能力。", "真正拉开差距的，往往不是一时爆发，而是能不能长期调整自己。"),
        (r"那些能够持续复盘、持续修正、持续精进的人，往往更容易在复杂环境中找到自己的节奏，并不断放大自己的优势。", "那些愿意持续复盘、修正、往前走的人，更容易在变化里找到自己的节奏，把优势慢慢做出来。"),
        (r"相反，如果一个人只是重复旧有路径、依赖惯性前进，那么即便看起来很忙，也很可能只是停留在低质量勤奋的循环之中。", "如果一个人只是按老路子走，靠惯性往前推，那看起来再忙，也可能只是在低质量勤奋里打转。"),
        (r"所以我想告诉大家的是，", "所以我想说的是，"),
        (r"未来真正重要的能力，不只是执行力，不只是学习力，也不只是单点突破的能力，而是把认知、方法、反馈和行动连接起来，形成一个真正可以持续运转的成长闭环。", "以后真正重要的，不只是执行力、学习力这些单点能力，更重要的是能不能把认知、方法、反馈和行动连起来，形成自己的成长闭环。"),
        (r"只有这样，一个人才有可能在变化中不断完成自我更新，并在长期竞争中获得更大的确定性。", "这样你才有机会在变化里不断更新自己，也更容易在长期里站稳。"),
        (r"如果你最近也正在经历迷茫、焦虑或者迟迟找不到突破口的阶段，", "如果你最近也有点迷茫、焦虑，或者一直找不到突破口，"),
        (r"我真心希望你可以认真想一想：", "不妨停下来想一想："),
        (r"你现在所坚持的东西，到底是在帮助你走向更高质量的成长，还是只是在消耗你的时间和注意力。", "你现在坚持的东西，到底真的在帮你成长，还是只是在消耗你的时间和注意力。"),
        (r"希望今天这段分享，能够给你带来一点新的思考和启发。", "希望这段话能给你一点启发。"),
        (r"如果你也有类似感受，欢迎在评论区分享你的看法。", "如果你也有类似感受，评论区聊聊。"),
        (r"很多人之所以一直没有办法真正建立起稳定的自律体系，核心原因并不是缺少目标感，而是没有找到一套能够持续运行的行为触发机制。?", "很多人一直没办法稳定自律，可能不是因为目标不够清楚，而是没有找到一个能让自己坚持下去的具体做法。"),
        (r"真正有效的自律，从来不是依靠短期情绪驱动，而是通过环境设计、即时反馈和低成本启动，逐步形成一种可以被长期复用的生活系统。?", "真正能坚持下来的自律，不能只靠一时兴起。更现实的做法是把环境调整好，及时给自己一点反馈，再从很小的动作开始，慢慢变成不用太费劲也能持续的习惯。"),
        (r"在这个快节奏的时代，我们总是在不断追逐目标，却常常忽略了生活本身真正值得被感知的细节。?", "最近越来越觉得，人一忙起来，就很容易忘了好好感受生活。"),
        (r"或许真正的松弛感，并不是逃离当下，而是在每一个普通瞬间里重新找回与自己相处的能力。?", "松弛感好像也不是非要逃到哪里去，而是在普通的一天里，还能留一点时间跟自己待一会儿。"),
        (r"在数字化浪潮持续深入企业经营全流程的今天，?", "现在很多团队都在用各种数字化工具。"),
        (r"越来越多团队开始意识到，单一工具已经无法满足复杂业务场景下的协同提效需求。?", "但用着用着大家会发现，只靠一个单点工具，很难把复杂协作真正跑顺。"),
        (r"我们的智能协作平台通过多模块能力整合、自动化流程编排以及数据驱动的决策支持，?", "我们这个智能协作平台，主要是把模块能力、流程自动化和数据分析放在一起，"),
        (r"帮助企业构建更加高效、稳定、可持续的组织协同体系，?", "帮团队少一点来回沟通，多一点清楚的推进节奏，"),
        (r"从而在激烈的市场竞争中持续释放增长潜能。?", "最后把协作效率和业务增长都往前推一推。"),
        (r"在快节奏生活方式不断升级的当下，?", "最近护肤步骤一多，我反而更在意有没有真的省心、好用。"),
        (r"越来越多精致女性开始关注日常护肤流程中的高效修护体验。?", "像精华这种每天都会用的东西，我会更看重修护感和稳定度。"),
        (r"这款精华通过多重植萃成分协同作用，深入肌肤底层，帮助改善干燥、暗沉与屏障脆弱等多维度肌肤问题，让肌肤在持续使用中焕发自然光彩。?", "这支用下来比较明显的是保湿感不错，干燥和暗沉的时候上脸也不会有负担。坚持用一段时间，皮肤状态会更稳一点。"),
        (r"我们正在寻找一位具备高度自驱力、优秀沟通协作能力以及复杂问题拆解能力的([^。]*?)候选人。?", r"我们想找一位\1，最好是比较主动、沟通顺畅，也能把复杂问题拆清楚的人。"),
        (r"你将深度参与业务需求洞察、产品方案设计、跨部门资源协调与项目落地推进，并通过数据分析与用户反馈持续优化产品体验，推动业务目标实现。?", "你会一起看业务需求、做产品方案，也会和不同团队对齐推进。后面还需要结合数据和用户反馈，把产品体验继续打磨好。"),
        (r"本产品采用高品质亲肤面料，结合人体工学剪裁设计，兼具舒适性、透气性与时尚表现力。?", "这件衣服用的是比较亲肤的面料，版型也做得比较贴合日常穿着。"),
        (r"这款产品采用高品质原材料与先进工艺打造，兼顾功能性、舒适性与审美表现。?", "这款东西用料和做工都比较扎实，功能性、舒适度和外观也都兼顾到了。"),
        (r"无论是日常通勤、休闲出行还是居家穿搭，都能够为用户提供更加自在、轻盈且高级的穿着体验。?", "平时通勤、出门或者在家穿都可以，整体是那种轻松、不紧绷，也不太挑场景的感觉。"),
        (r"无论是在日常通勤、家庭使用还是多场景切换中，都能够为用户提供稳定、轻松且高效率的使用体验。?", "平时通勤、在家用，或者不同场景切换的时候，用起来都比较顺手，不会让人觉得麻烦。"),
        (r"我们希望通过更加细致的设计与持续优化的细节打磨，为用户带来真正意义上的品质升级。?", "我们更想把细节慢慢做好，让你用的时候能明显感觉到体验更顺一点。"),
        (r"为了帮助大家系统化理解短视频内容增长方法论，我们将在今晚八点开启一场高价值直播分享。?", "今晚八点我会开一场直播，聊聊短视频内容到底怎么做增长。"),
        (r"本次直播将围绕账号定位、内容选题、流量承接与转化路径展开深度拆解，帮助大家构建可持续增长的内容运营体系。?", "会讲账号定位、选题、流量怎么接住，以及后面怎么转化。尽量讲得具体一点，别只停留在概念上。"),
        (r"为了帮助大家[^。！？]{0,120}?我们将在今晚八点开启一场高价值直播分享，?", "今晚八点我会开一场直播，想和大家聊聊短视频到底怎么做增长。"),
        (r"围绕账号定位、内容策划、流量获取与转化路径展开深度讲解，欢迎大家准时进入直播间共同学习成长。?", "会讲账号定位、内容怎么策划、流量和转化这些问题，感兴趣的话可以来直播间聊聊。"),
        (r"如果你正在经历表达能力不足、公众演讲紧张以及职场沟通效率低下等问题，?", "如果你一到发言就紧张，或者在职场里总觉得自己说不清楚，"),
        (r"那么这套系统化表达训练课程将帮助你从底层逻辑出发，全面提升结构化表达、临场反应和影响力输出能力，真正实现个人竞争力跃迁。?", "这套表达课会从怎么组织内容、怎么临场回应、怎么把话说得更有重点这几块练起，让你在沟通和汇报时更稳一点。"),
        (r"尊敬的客户您好，?", "您好，"),
        (r"您好，针对您反馈的订单延迟配送问题，?", "您好，您反馈的订单配送慢这个问题，"),
        (r"针对您反馈的订单延迟配送问题，?", "您反馈的订单配送慢这个问题，"),
        (r"关于您反馈的物流延迟问题，我们已第一时间联系相关承运方进行核实处理。?", "您反馈的物流慢这个问题，我这边已经联系承运方核实了。"),
        (r"我司已第一时间协调相关部门进行核查处理。?", "我这边已经联系相关同事核实了。"),
        (r"目前物流链路正在持续推进中，预计将在24小时内同步最新进展，感谢您的理解与支持。?", "目前物流这边还在跟进，预计24小时内给您同步最新进展，也谢谢您的理解。"),
        (r"目前包裹正在加急转运途中，具体送达时效以物流页面更新为准。?", "目前包裹还在加急转运，具体什么时候送到，还要以物流更新为准。"),
        (r"给您带来的不便我们深表歉意，感谢您的理解与支持，如后续您有任何疑问，欢迎随时与我们联系。?", "给您添麻烦了，确实不好意思。后面如果还有问题，随时找我。"),
        (r"关于您反馈的退款诉求，我们已经第一时间提交相关部门进行核实处理。?", "您反馈的退款问题我们已经提交处理了。"),
        (r"针对您反馈的退款诉求，我们已经高度重视并同步相关部门进行综合核实。?", "您反馈的退款问题，我这边已经在和相关同事核实了。"),
        (r"针对您反馈的([^，。]{1,30})，我们已经高度重视并同步相关部门进行综合核实。?", r"您反馈的\1，我这边已经在和相关同事核实了。"),
        (r"当前([^，。]{1,24})正在有序推进中，请您耐心等待，我们会在确认后第一时间为您提供完整处理结果。?", r"目前\1还在推进中，有结果后我会尽快同步给您。"),
        (r"感谢您的理解与支持，我们将持续为您提供优质服务。?", "谢谢您的理解，我也会继续跟进这件事。"),
        (r"后续将在规定时效内为您同步最新进展，请您耐心等待。?", "这边有进展我会及时同步给您，您不用反复催。"),
        (r"感谢您的理解与支持，祝您生活愉快。?", "也谢谢您理解。"),
        (r"亲爱的各位伙伴，大家好。?", "大家好，"),
        (r"亲爱的朋友们大家好！?", "大家好，"),
        (r"为了进一步提升社群整体互动质量与内容传播效率，我们将于([^，。]+)正式开展一次主题分享活动。?", r"\1我们会做一场主题分享。"),
        (r"本次活动将围绕个人成长、认知升级与行动闭环展开系统化交流，希望大家积极参与，共同打造高质量学习型社群。?", "这次主要聊个人成长和行动复盘，感兴趣的朋友可以一起参加，现场也欢迎大家多交流。"),
        (r"本次活动将围绕([^，。]+)展开系统化交流，希望大家积极参与，共同打造高质量学习型社群。?", r"这次主要聊\1，感兴趣的朋友可以一起参加，现场也欢迎大家多交流。"),
        (r"围绕([^，。]+)展开系统化交流", r"聊聊\1"),
        (r"共同打造高质量学习型社群", "也让群里的交流更实在一点"),
        (r"尊敬的用户您好，?", "您好，"),
        (r"非常抱歉本次服务体验未能达到您的预期。?", "这次体验没做好，先跟您说声抱歉。"),
        (r"针对您反馈的问题，我们已第一时间高度重视并同步相关部门进行全面排查与优化。?", "您反馈的问题我们已经记下来了，也会尽快和相关同事一起看具体原因。"),
        (r"后续我们也将持续完善服务流程，努力为每一位用户提供更加优质、高效、贴心的服务体验。?", "后面我们会把这个环节再梳理一下，避免类似情况反复出现。"),
        (r"感谢您的宝贵反馈，祝您生活愉快。?", "也谢谢您愿意把问题说出来。"),
        (r"很多人之所以一直没有办法真正建立起([^，。]+)，核心原因并不是([^，。]+)，而是([^。]+)。?", r"很多人一直没能真正建立起\1，不一定是因为\2，更多是因为\3。"),
        (r"真正有效的([^，。]+)，从来不是依靠([^，。]+)，而是通过([^，。]+)，逐步形成([^。]+)。?", r"真正有效的\1，靠的不是\2，而是\3，慢慢形成\4。"),
    ]
    if natural:
        replacements.extend(
            [
                (r"这几年变化真的很快，普通人真的得重新想想", "这几年变化真的很快，我越来越觉得，普通人得重新想想"),
                (r"所以我想说的是，", "说实话，我越来越觉得，"),
                (r"很多人一直没办法稳定自律，可能不是因为目标不够清楚，而是没有找到一个能让自己坚持下去的具体做法。", "说白了，很多人不是不想自律，也不是完全没目标，而是一直没找到一个真的能坚持下去的方法。"),
                (r"真正能坚持下来的自律，不能只靠一时兴起。更现实的做法是把环境调整好，及时给自己一点反馈，再从很小的动作开始，慢慢变成不用太费劲也能持续的习惯。", "真正能坚持下来的自律，靠的也不是突然打鸡血。更有用的办法，反而是把环境先调整好，给自己一点及时反馈，再从特别小的动作开始。这样时间久了，它才会慢慢变成一种没那么费劲的习惯。"),
                (r"最近越来越觉得，人一忙起来，就很容易忘了好好感受生活。", "最近越来越觉得，人一忙起来，真的很容易忘了好好感受生活。"),
                (r"松弛感好像也不是非要逃到哪里去，而是在普通的一天里，还能留一点时间跟自己待一会儿。", "所谓松弛感，好像也不是非要逃到哪里去。更多时候，就是在很普通的一天里，还能留一点时间跟自己待一会儿。"),
                (r"我们这个智能协作平台，主要是把模块能力、流程自动化和数据分析放在一起，", "我们这个智能协作平台，简单说就是把模块能力、流程自动化和数据分析放在一起，"),
                (r"帮团队少一点来回沟通，多一点清楚的推进节奏，", "让团队少一点来回沟通，多一点清楚的推进节奏，"),
                (r"最近护肤步骤一多，我反而更在意有没有真的省心、好用。", "最近护肤步骤一多，我反而更在意这东西到底省不省心、好不好用。"),
                (r"像精华这种每天都会用的东西，我会更看重修护感和稳定度。", "像精华这种每天都会用的东西，我最看重的还是修护感和稳定度。"),
                (r"你会一起看业务需求、做产品方案，也会和不同团队对齐推进。", "你会一起看业务需求、做产品方案，也要和不同团队对齐推进。"),
                (r"这件衣服用的是比较亲肤的面料，版型也做得比较贴合日常穿着。", "这件衣服摸起来比较舒服，版型也更适合日常穿。"),
                (r"今晚八点我会开一场直播，聊聊短视频内容到底怎么做增长。", "今晚八点我会开一场直播，聊聊短视频到底怎么做增长。"),
                (r"这套表达课会从怎么组织内容、怎么临场回应、怎么把话说得更有重点这几块练起，让你在沟通和汇报时更稳一点。", "这套表达课会从组织内容、临场回应、把重点说清楚这几块练起，让你沟通和汇报时更稳一点。"),
                (r"您反馈的退款问题我们已经提交处理了。", "您反馈的退款问题，我这边已经提交处理了。"),
                (r"大家好，", "大家好，跟大家同步一下，"),
                (r"这次主要聊个人成长和行动复盘，感兴趣的朋友可以一起参加，现场也欢迎大家多交流。", "这次主要聊个人成长和行动复盘，不会太正式，感兴趣的朋友可以一起来，现场也欢迎大家多聊聊。"),
                (r"这次主要聊([^，。]+)，感兴趣的朋友可以一起参加，现场也欢迎大家多交流。", r"这次主要聊\1，不会太正式，感兴趣的朋友可以一起来，现场也欢迎大家多聊聊。"),
                (r"这次体验没做好，先跟您说声抱歉。", "这次体验没做好，先跟您说声抱歉。"),
                (r"您反馈的问题我们已经记下来了，也会尽快和相关同事一起看具体原因。", "您反馈的问题我们已经记下来了，我会尽快和相关同事一起看具体原因。"),
            ],
        )
    text = _apply_replacements(text, replacements)
    if repair:
        if {"template_tone", "source_template_carryover", "too_similar"} & set(failure_tags):
            text = _apply_generic_template_repairs(text, natural=natural)
            text = _apply_replacements(
                text,
                [
                    (r"重新想想成长这件事的根子上的问题", "重新想想成长这件事到底该怎么想"),
                    (r"成长这件事的更核心的问题", "成长这件事到底该怎么想"),
                    (r"低质量勤奋里打转", "只是在瞎忙" if natural else "只是在表面上忙"),
                    (r"形成自己的一套能转起来的方法", "形成一套适合自己的方法"),
                    (r"形成自己的一套能持续转起来的方法", "形成一套适合自己的方法"),
                    (r"把认知、方法、反馈和行动连起来", "把想法、方法、反馈和行动串起来"),
                    (r"把想法、方法、反馈和行动连起来", "把想法、方法、反馈和行动串起来"),
                    (r"说实话，我越来越觉得，", "我这段时间越来越觉得，" if natural else "说到底，"),
                ],
            )
        if "bad_splice" in failure_tags:
            text = cleanup_common_phrase_collisions(text)
        if "overcompressed" in failure_tags and source_reference:
            source_reference = _compact_copy(source_reference)
            if _compact_len(text) < _compact_len(source_reference) * 0.7:
                source_paragraphs = [part.strip() for part in source_reference.split("\n\n") if part.strip()]
                text_paragraphs = [part.strip() for part in text.split("\n\n") if part.strip()]
                for paragraph in source_paragraphs:
                    if paragraph and all(_similarity_ratio(paragraph, existing) < 0.72 for existing in text_paragraphs):
                        patched = rewrite_longform_copy(
                            paragraph,
                            natural=natural,
                            repair=False,
                        )
                        text = text.rstrip() + "\n\n" + patched.strip()
                        break
    text = re.sub(r"不只是([^，。]+)，不只是([^，。]+)，也不只是([^，。]+)，而是", r"不只是\1、\2、\3，更重要的是", text)
    text = re.sub(r"([。！？])(?=[^\n])", r"\1\n\n", text)
    text = re.sub(r"\n{3,}", "\n\n", text).strip()
    return text


def build_longform_rewrite_heuristics(
    task: str,
    source_text: str,
    current_best_text: str,
    failure_tags: list[str],
    *,
    repair: bool,
) -> list[tuple[str, str]]:
    if not looks_like_longform_rewrite(task, source_text):
        return []
    base_text = current_best_text if repair else source_text
    natural = rewrite_longform_copy(
        base_text,
        natural=True,
        repair=repair,
        source_reference=source_text,
        failure_tags=failure_tags,
    )
    balanced = rewrite_longform_copy(
        base_text,
        natural=False,
        repair=repair,
        source_reference=source_text,
        failure_tags=failure_tags,
    )
    return [
        ("heuristic-natural", natural),
        ("heuristic-balanced", balanced),
    ]


def build_short_reply_heuristics(
    task: str,
    source_text: str,
    hard_constraints: dict[str, Any],
) -> list[tuple[str, str]]:
    lowered = task.lower()
    if "邮件" in task or "email" in lowered:
        return []
    if not looks_like_short_message_task(task, hard_constraints):
        return []
    must_include = [str(item).strip() for item in hard_constraints.get("must_include") or [] if str(item).strip()]
    max_chars = hard_constraints.get("max_chars")
    time_term = infer_time_term(must_include, f"{task} {source_text}")
    deadline = deadline_term(time_term)
    required_terms = [item for item in must_include if item != time_term]
    is_internal = any(marker in task for marker in ("老板", "上级", "领导"))
    is_service = any(marker in task for marker in ("售后", "投诉", "安抚", "退款", "退换"))
    is_app_push = any(marker in task for marker in ("App", "APP", "推送", "直播课")) and any(
        marker in task for marker in ("直播", "课程", "课")
    )
    is_repair_request = any(marker in task for marker in ("房东", "维修", "报修", "空调", "不制冷"))
    is_peer_sync = (
        any(marker in task for marker in ("同事", "飞书", "测试环境", "接口字段"))
        and any(marker in task for marker in ("同步", "调整", "变更"))
    )
    is_social_decline = is_social_decline_task(task)
    joined_terms = "、".join(required_terms)
    first_term = required_terms[0] if required_terms else ""
    tail_terms = "、".join(required_terms[1:]) if len(required_terms) > 1 else ""
    issue, action = infer_progress_context(task)
    service_issue = infer_service_issue(task, source_text) if is_service else ""

    if is_app_push:
        variants = [
            ("heuristic-natural", f"{time_term}直播课开讲，记得来听。"),
            ("heuristic-balanced", f"{time_term}有直播课，记得来听。"),
        ]
    elif is_social_decline:
        variants = [
            (
                "heuristic-natural",
                f"{time_term}我先不去啦，今天得赶项目，实在有点抽不开。你们先吃，下次我补上。"
                if "项目" in task
                else f"{time_term}我先不去啦，今天这边实在有点抽不开。你们先玩，下次我补上。"
            ),
            (
                "heuristic-balanced",
                f"{time_term}我可能去不了了，今天项目得赶一下。你们先好好吃，下次我再约。"
                if "项目" in task
                else f"{time_term}我可能去不了了，今天确实有点抽不开。你们先玩，下次我再约。"
            ),
        ]
    elif is_internal:
        if issue or action:
            variants = [
                (
                    "heuristic-natural",
                    f"老板，{issue or joined_terms or '这件事'}这块{action or '今天已经跟进'}，{time_term}我再同步进展。"
                ),
                (
                    "heuristic-balanced",
                    f"老板，{issue or joined_terms or '这件事'}目前还在处理，{action or '今天已经跟进'}，{time_term}给您同步结果。"
                ),
            ]
        else:
            variants = [
                (
                    "heuristic-natural",
                    f"老板，今天和{first_term}已经对过了，{time_term}我把{tail_terms or '进展'}整理好发您。"
                    if first_term
                    else f"老板，今天这件事我已经跟进过了，{time_term}我把更明确的进展发您。"
                ),
                (
                    "heuristic-balanced",
                    f"老板，今天和{first_term}这边已经沟通过了，{time_term}我会把{tail_terms or '进展'}整理后发您。"
                    if first_term
                    else f"老板，今天这件事我已经对过一轮了，{time_term}给您同步更明确的进展。"
                ),
            ]
    elif is_service:
        variants = [
            (
                "heuristic-natural",
                f"您好，您反馈的{service_issue or '这个问题'}我这边已经登记并在核实了，目前还在处理中。有结果后我会尽快同步给您，后续有问题也可以随时联系我。"
            ),
            (
                "heuristic-balanced",
                f"您好，关于{service_issue or '这个问题'}，这边已经登记并在核实，目前还在处理中。有结果后会尽快同步给您，谢谢您的耐心。"
            ),
        ]
    elif is_repair_request:
        issue_text = "空调不制冷" if "空调" in task and "不制冷" in task else (joined_terms or "这边")
        variants = [
            (
                "heuristic-natural",
                f"您好，家里{issue_text}，想问下{time_term}方便安排师傅过来维修吗？"
            ),
            (
                "heuristic-balanced",
                f"您好，{issue_text}，想约{time_term}维修，您看这个时间方便安排吗？"
            ),
        ]
    elif is_peer_sync:
        variants = [
            (
                "heuristic-natural",
                f"接口字段今天有调整，麻烦{deadline}同步到测试环境，我这边后续按这个版本对。"
            ),
            (
                "heuristic-balanced",
                f"接口字段刚调整过，麻烦{deadline}帮忙同步到测试环境，我这边后面按新版本测。"
            ),
        ]
    elif "客户" in task:
        variants = [
            (
                "heuristic-natural",
                f"您好，这边和{joined_terms or '这件事'}相关的内容还在跟进，预计{time_term}给您更明确的回复。"
            ),
            (
                "heuristic-balanced",
                f"您好，关于{joined_terms or '这件事'}我这边还在确认，{deadline}给您同步更明确的进展。"
            ),
        ]
    else:
        variants = [
            (
                "heuristic-natural",
                f"您好，{joined_terms or '这件事'}我这边还在跟进，预计{time_term}给您明确回复。"
            ),
            (
                "heuristic-balanced",
                f"您好，目前{joined_terms or '这件事'}还在确认中，{deadline}给您同步更明确的进展。"
            ),
        ]

    filtered: list[tuple[str, str]] = []
    for profile, text in variants:
        candidate = text.strip()
        if max_chars is not None and len(candidate.replace(" ", "").replace("\n", "")) > int(max_chars):
            continue
        if must_include and any(item not in candidate for item in must_include):
            continue
        filtered.append((profile, candidate))
    return filtered


def build_generate_heuristics(
    task: str,
    hard_constraints: dict[str, Any],
    baseline_text: str,
) -> list[tuple[str, str]]:
    must_include = [str(item).strip() for item in hard_constraints.get("must_include") or [] if str(item).strip()]
    max_chars = hard_constraints.get("max_chars")
    is_email = "邮件" in task or "email" in task.lower()
    if not is_email:
        short_variants = build_short_reply_heuristics(task, "", hard_constraints)
        baseline_compact = baseline_text.strip()
        return [
            (profile, candidate)
            for profile, candidate in short_variants
            if candidate.strip() != baseline_compact
        ]

    time_term = infer_time_term(must_include, task)
    deadline = deadline_term(time_term)
    other_terms = [item for item in must_include if item != time_term]
    primary_term = other_terms[0] if other_terms else ""
    joined_terms = "、".join(other_terms)
    variants: list[tuple[str, str]]

    if any(marker in task for marker in ("合作方", "合同附件", "催资料", "项目排期")):
        target = primary_term or "合同编号"
        variants = [
            (
                "heuristic-natural",
                f"您好，{target}对应的附件这边还没收到，项目排期会受影响。麻烦您{deadline}补一下，谢谢。",
            ),
            (
                "heuristic-balanced",
                f"您好，{target}相关附件目前还没收到，可能影响项目排期。麻烦{deadline}补充给我们，谢谢。",
            ),
        ]
    elif "客户" in task:
        variants = [
            (
                "heuristic-natural",
                f"您好，关于{joined_terms or '这件事'}我这边还在确认，预计{time_term}给您一个更明确的回复。",
            ),
            (
                "heuristic-balanced",
                f"您好，{joined_terms or '相关事项'}目前还在推进中，预计{deadline}给您同步更明确的进展。",
            ),
        ]
    else:
        variants = [
            (
                "heuristic-natural",
                f"您好，关于{joined_terms or '这件事'}我这边还在跟进，预计{deadline}给您同步明确进展。",
            ),
            (
                "heuristic-balanced",
                f"您好，{joined_terms or '相关事项'}目前还在确认，预计{deadline}给您回复，谢谢。",
            ),
        ]

    filtered: list[tuple[str, str]] = []
    baseline_compact = baseline_text.strip()
    for profile, text in variants:
        candidate = text.strip()
        if candidate == baseline_compact:
            continue
        if max_chars is not None and len(candidate.replace(" ", "").replace("\n", "")) > int(max_chars):
            continue
        if must_include and any(item not in candidate for item in must_include):
            continue
        filtered.append((profile, candidate))
    return filtered


def build_rewrite_heuristics(
    task: str,
    source_text: str,
    current_best_text: str,
    hard_constraints: dict[str, Any],
    failure_tags: list[str],
    *,
    revision_mode: str,
) -> list[tuple[str, str]]:
    repair = revision_mode == "repair"
    heuristics = build_email_rewrite_heuristics(
        task,
        source_text,
        current_best_text,
        failure_tags,
        repair=repair,
    )
    if heuristics:
        return heuristics
    short_heuristics = build_short_reply_heuristics(
        task,
        current_best_text if repair else source_text,
        hard_constraints,
    )
    if short_heuristics:
        return short_heuristics
    if looks_like_longform_rewrite(task, source_text):
        if any(marker in task for marker in COPY_TASK_MARKERS):
            heuristics = build_longform_rewrite_heuristics(
                task,
                source_text,
                current_best_text,
                failure_tags,
                repair=repair,
            )
            if heuristics:
                return heuristics
    heuristics = build_longform_rewrite_heuristics(
        task,
        source_text,
        current_best_text,
        failure_tags,
        repair=repair,
    )
    if heuristics:
        return heuristics
    base_text = current_best_text if repair else source_text
    return build_short_reply_heuristics(task, base_text, hard_constraints)


def cleanup_common_phrase_collisions(text: str) -> str:
    cleaned = text
    replacements = [
        (
            r"稍后前(?=给|向|为|把|会|同步|回复)",
            "稍后",
        ),
        (
            r"感谢您的耐心，如后续[^。！？]*如有需要，欢迎随时联系我。?",
            "感谢您的耐心，如有其他问题，欢迎随时联系我。",
        ),
        (
            r"感谢您的耐心，如后续[^。！？]*欢迎随时联系我。?",
            "感谢您的耐心，如有其他问题，欢迎随时联系我。",
        ),
        (
            r"如后续[^。！？]*如有需要，欢迎随时联系我。?",
            "如有其他问题，欢迎随时联系我。",
        ),
        (
            r"如后续[^。！？]*欢迎随时联系我。?",
            "如有其他问题，欢迎随时联系我。",
        ),
        (
            r"如后续[^。！？]*如有需要，欢迎随时联系。?",
            "如有其他问题，欢迎随时联系。",
        ),
    ]
    for pattern, repl in replacements:
        cleaned = re.sub(pattern, repl, cleaned)
    return cleaned


def should_force_model_retry(
    *,
    session_mode: str,
    round_number: int,
    max_rounds: int,
    round_candidates: list[dict[str, Any]],
    heuristic_variants: list[tuple[str, str]],
) -> bool:
    if session_mode != "rewrite":
        return False
    if round_number >= max_rounds:
        return False
    if not heuristic_variants or not round_candidates:
        return False
    all_hard_fail = all(bool((candidate.get("score") or {}).get("hard_fail")) for candidate in round_candidates)
    if not all_hard_fail:
        return False
    seen_tags = {
        tag
        for candidate in round_candidates
        for tag in (candidate.get("failure_tags") or [])
    }
    return bool(seen_tags.intersection(QUALITY_GATE_RETRY_TAGS))


def apply_best_so_far_guardrails(
    *,
    candidate_text: str,
    current_best_text: str,
    source_text: str,
    score_payload: dict[str, Any],
    failure_tags_out: list[str],
    enforce_continuity: bool,
) -> tuple[dict[str, Any], list[str]]:
    if not enforce_continuity:
        score_payload = dict(score_payload)
        score_payload["notes"] = list(score_payload.get("notes") or [])
        unique_tags: list[str] = []
        seen_tags: set[str] = set()
        for tag in failure_tags_out:
            if tag not in seen_tags:
                seen_tags.add(tag)
                unique_tags.append(tag)
        return score_payload, unique_tags

    notes = list(score_payload.get("notes") or [])
    penalty = 0.0

    current_best_templates = _template_phrase_hits(current_best_text)
    candidate_templates = _template_phrase_hits(candidate_text)
    if len(candidate_templates) > len(current_best_templates):
        notes.append("candidate reintroduces more template phrases than current best")
        failure_tags_out.append("regressed_from_best")
        penalty += 0.08

    source_to_best = _similarity_ratio(source_text, current_best_text)
    source_to_candidate = _similarity_ratio(source_text, candidate_text)
    if source_to_candidate >= source_to_best + 0.035 and source_to_candidate >= 0.78:
        notes.append(
            f"candidate drifts back toward source wording (ratio={source_to_candidate:.3f} > best={source_to_best:.3f})",
        )
        failure_tags_out.append("regressed_from_best")
        penalty += 0.1

    current_best_len = _compact_len(current_best_text)
    candidate_len = _compact_len(candidate_text)
    if current_best_len >= 50 and candidate_len < current_best_len * 0.72:
        notes.append(f"candidate compresses current best too aggressively (ratio={candidate_len / max(current_best_len, 1):.3f})")
        if "overcompressed" not in failure_tags_out:
            failure_tags_out.append("overcompressed")
        failure_tags_out.append("regressed_from_best")
        penalty += 0.1

    best_paragraphs = [part.strip() for part in current_best_text.split("\n\n") if part.strip()]
    candidate_paragraphs = [part.strip() for part in candidate_text.split("\n\n") if part.strip()]
    if len(best_paragraphs) >= 3 and len(candidate_paragraphs) + 1 < len(best_paragraphs):
        notes.append("candidate collapses paragraph structure compared with current best")
        failure_tags_out.append("regressed_from_best")
        penalty += 0.06

    unique_tags: list[str] = []
    seen_tags: set[str] = set()
    for tag in failure_tags_out:
        if tag not in seen_tags:
            seen_tags.add(tag)
            unique_tags.append(tag)

    if penalty > 0:
        score_payload = dict(score_payload)
        score_payload["notes"] = notes
        score_payload["final_score"] = round(max(0.0, float(score_payload.get("final_score") or 0.0) - penalty), 6)
    else:
        score_payload = dict(score_payload)
        score_payload["notes"] = notes
    return score_payload, unique_tags


def build_heuristic_candidate(
    *,
    run_dir: Path,
    spec: dict[str, Any],
    task: str,
    source_text: str,
    current_best_text: str,
    current_best_score_value: float,
    enforce_continuity: bool,
    round_number: int,
    candidate_index: int,
    profile: str,
    candidate_text: str,
) -> dict[str, Any]:
    stem = f"round-{round_number:03d}-candidate-{candidate_index:02d}"
    candidate_path = run_dir / "rounds" / f"{stem}.txt"
    score_path = run_dir / "rounds" / f"{stem}.score.json"
    cleaned_text = cleanup_common_phrase_collisions(candidate_text.strip())
    write_text(candidate_path, cleaned_text)
    score_obj = score_candidate(spec, cleaned_text, source_text)
    score_payload = score_obj.as_dict()
    failure_tags_out = extract_failure_tags(
        task,
        cleaned_text,
        score_payload,
        baseline_score=current_best_score_value,
    )
    if cleaned_text.strip() == current_best_text.strip() and "copied_baseline" not in failure_tags_out:
        failure_tags_out.append("copied_baseline")
    score_payload, failure_tags_out = apply_best_so_far_guardrails(
        candidate_text=cleaned_text,
        current_best_text=current_best_text,
        source_text=source_text,
        score_payload=score_payload,
        failure_tags_out=failure_tags_out,
        enforce_continuity=enforce_continuity,
    )
    write_json(
        score_path,
        {
            "candidate_path": str(candidate_path.resolve()),
            "generation_path": "",
            "heuristic": True,
            **score_payload,
        },
    )
    return {
        "candidate_index": candidate_index,
        "profile": profile,
        "text": cleaned_text,
        "candidate_path": str(candidate_path.resolve()),
        "generation_path": "",
        "score_path": str(score_path.resolve()),
        "score": score_payload,
        "failure_tags": failure_tags_out,
    }


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Prepare a run from a raw brief and execute a visible multi-candidate humanize loop.",
    )
    parser.add_argument("--text", default=None)
    parser.add_argument("--input", type=Path, default=None)
    parser.add_argument("--baseline-text", default=None)
    parser.add_argument("--challenger-text", default=None)
    parser.add_argument("--run-dir", type=Path, default=None)
    parser.add_argument("--output-root", type=Path, default=Path("./runs"))
    parser.add_argument(
        "--max-rounds",
        type=int,
        default=None,
        help=f"Maximum optimization rounds, clamped to 1..{MAX_ALLOWED_ROUNDS}. Defaults to {DEFAULT_MAX_ROUNDS} or HUMANIZE_MAX_ROUNDS.",
    )
    args = parser.parse_args()

    if args.text is None and args.input is None:
        raise ValueError("Provide --text or --input")

    raw_text = args.text if args.text is not None else args.input.read_text(encoding="utf-8")
    payload = build_payload(raw_text)
    spec = payload["spec"]
    task = str(payload["parsed"]["task"] or "run")
    run_dir = args.run_dir.resolve() if args.run_dir else create_run_dir(args.output_root.resolve(), task)
    run_dir.mkdir(parents=True, exist_ok=True)
    (run_dir / "rounds").mkdir(exist_ok=True)

    spec_path = run_dir / "spec.yaml"
    source_path = run_dir / "source.txt"
    parse_path = run_dir / "parse-result.json"
    brief_path = run_dir / "user-brief.txt"

    write_text(
        spec_path,
        yaml.safe_dump(
            spec,
            allow_unicode=True,
            sort_keys=False,
            default_flow_style=False,
        ),
    )
    write_text(source_path, payload["parsed"]["original"])
    write_json(parse_path, payload)
    write_text(brief_path, raw_text.strip() + "\n")

    session_mode = payload["session_mode"]
    generation_backend_error = ""
    try:
        generation_backend = discover_generation_backend()
    except Exception as exc:
        generation_backend_error = str(exc)
        generation_backend = {
            "kind": "heuristic-only",
            "base_url": "heuristic-only://unavailable",
            "model": "heuristic-only",
        }
    base_url = generation_backend["base_url"]
    model = generation_backend["model"]
    generation_available = generation_backend.get("kind") != "heuristic-only"

    state = load_state()
    state_before = snapshot_state(state)
    state_before_path = run_dir / "strategy-state.before.json"
    state_after_path = run_dir / "strategy-state.after.json"
    write_json(state_before_path, state_before)

    original = payload["parsed"]["original"]
    hard_constraints = payload["parsed"]["hard_constraints"]
    margin = float(((spec.get("evaluator") or {}).get("minimum_improvement")) or 0.015)

    baseline_override = (args.baseline_text or "").strip()
    challenger_override = (args.challenger_text or "").strip()
    last_success_profile = str(state.get("last_success_profile") or "steady") or "steady"
    baseline_profile = last_success_profile if last_success_profile != "repair" else "steady"
    initial_directives = state_directives(state, [])
    (baseline_system, baseline_user), _ = build_generation_prompts(
        task=task,
        hard_constraints=hard_constraints,
        original=original,
        mode=session_mode,
        baseline_profile=baseline_profile,
        strategy_directives=initial_directives,
    )

    if baseline_override:
        baseline_text = baseline_override
    elif session_mode == "rewrite":
        baseline_text = original
    elif looks_like_short_message_task(task, hard_constraints):
        baseline_text = build_fallback_baseline(task, hard_constraints)
        write_json(
            run_dir / "baseline.generation.json",
            {
                "heuristic": True,
                "reason": "short message baseline",
                "baseline_text": baseline_text,
            },
        )
    elif not generation_available:
        baseline_text = build_fallback_baseline(task, hard_constraints)
        write_json(
            run_dir / "baseline.generation.json",
            {
                "fallback": True,
                "reason": f"generation backend unavailable: {generation_backend_error}",
                "baseline_text": baseline_text,
            },
        )
    else:
        try:
            baseline_text, baseline_response = call_and_extract_candidate(
                base_url=base_url,
                model=model,
                system_prompt=baseline_system,
                user_prompt=baseline_user,
                temperature=0.25,
                max_tokens_sequence=[220, 320, 420],
                hard_constraints=hard_constraints,
                require_hard_constraints=True,
                min_chars_hint=40 if ("邮件" in task or "email" in task.lower()) else (18 if "客户" in task else 14),
            )
            write_json(run_dir / "baseline.generation.json", baseline_response)
        except Exception as exc:
            baseline_text = build_fallback_baseline(task, hard_constraints)
            write_json(
                run_dir / "baseline.generation.json",
                {
                    "fallback": True,
                    "reason": str(exc),
                    "baseline_text": baseline_text,
                },
            )

    initial_baseline_text = baseline_text
    source_text = original or initial_baseline_text
    baseline_score_obj = score_candidate(spec, baseline_text, source_text)
    baseline_score_payload = baseline_score_obj.as_dict()
    write_json(
        run_dir / "baseline.initial.score.json",
        {
            "candidate_path": str((run_dir / "baseline.txt").resolve()),
            "source_text": source_text,
            **baseline_score_payload,
        },
    )

    current_best_text = baseline_text
    current_best_score = baseline_score_obj
    current_failure_tags = extract_failure_tags(task, baseline_text, baseline_score_payload)
    session_trace: list[dict[str, Any]] = []
    selected_candidates: list[dict[str, Any]] = []
    improved_any = False
    run_budget = derive_run_budget(task, session_mode, args.max_rounds)
    max_rounds = int(run_budget["max_rounds"])
    challenger_count = int(run_budget["challenger_count"])

    final_kept_candidate: dict[str, Any] | None = None

    if challenger_override:
        override_score = score_candidate(spec, challenger_override, source_text)
        override_record = {
            "candidate_index": 1,
            "profile": "manual-override",
            "text": challenger_override,
            "candidate_path": "",
            "generation_path": "",
            "score_path": "",
            "score": override_score.as_dict(),
            "failure_tags": extract_failure_tags(
                task,
                challenger_override,
                override_score.as_dict(),
                baseline_score=current_best_score.final_score,
            ),
        }
        selected_candidates.append(override_record)
        delta = override_score.final_score - current_best_score.final_score
        improved = (not override_score.hard_fail) and delta >= margin
        session_trace.append(
            {
                "round": 1,
                "profiles": ["manual-override"],
                "failure_tags_in": [],
                "strategy_directives": initial_directives,
                "baseline_text": current_best_text,
                "baseline_score": current_best_score.as_dict(),
                "candidates": [override_record],
                "selected_candidate": override_record,
                "delta": round(delta, 6),
                "decision": "keep" if improved else "discard",
                "reason": "manual challenger override" if improved else "manual challenger override did not improve enough",
                "revision_mode": "rewrite",
                "base_text_kind": "source",
            },
        )
        if improved:
            improved_any = True
            current_best_text = challenger_override
            current_best_score = override_score
            final_kept_candidate = override_record
    else:
        for round_index in range(max_rounds):
            round_number = round_index + 1
            profiles = choose_profiles(state, current_failure_tags, round_index)[:challenger_count]
            directives = state_directives(state, current_failure_tags)
            round_candidates: list[dict[str, Any]] = []
            revision_mode = "repair" if session_mode == "rewrite" and improved_any else "rewrite"
            base_text_kind = "best_so_far" if revision_mode == "repair" else "source"
            enforce_continuity = revision_mode == "repair"
            if session_mode == "generate":
                heuristic_variants = build_generate_heuristics(task, hard_constraints, current_best_text)
            else:
                heuristic_variants = build_rewrite_heuristics(
                    task,
                    source_text,
                    current_best_text,
                    hard_constraints,
                    current_failure_tags,
                    revision_mode=revision_mode,
                )
            max_chars = int(hard_constraints.get("max_chars") or 0)
            is_short_chat = looks_like_short_message_task(task, hard_constraints) and max_chars and max_chars <= 120
            force_model_candidates = round_number > 1 and any(
                tag in current_failure_tags for tag in QUALITY_GATE_RETRY_TAGS
            )
            skip_model_candidates = (not force_model_candidates) and bool(heuristic_variants) and (
                session_mode == "generate"
                or looks_like_longform_rewrite(task, source_text)
                or is_short_chat
                or looks_like_professional_email(task, source_text)
            )

            if not generation_available:
                write_json(
                    run_dir / f"round-{round_number:03d}-generation-backend.json",
                    {
                        "kind": "heuristic-only",
                        "reason": generation_backend_error,
                        "message": "No generation backend was available; using heuristic candidates only.",
                    },
                )
            elif not skip_model_candidates:
                for candidate_index, profile in enumerate(profiles, start=1):
                    try:
                        candidate = generate_candidate(
                            run_dir=run_dir,
                            spec=spec,
                            session_mode=session_mode,
                            task=task,
                            hard_constraints=hard_constraints,
                            source_text=source_text,
                            current_best_text=current_best_text,
                            base_url=base_url,
                            model=model,
                            profile=profile,
                            strategy_directives=directives,
                            failure_tags=current_failure_tags,
                            revision_mode=revision_mode,
                            round_number=round_number,
                            candidate_index=candidate_index,
                            current_best_score_value=current_best_score.final_score,
                            enforce_continuity=enforce_continuity,
                        )
                    except Exception as exc:
                        candidate = {
                            "candidate_index": candidate_index,
                            "profile": profile,
                            "text": "",
                            "candidate_path": "",
                            "generation_path": "",
                            "score_path": "",
                            "score": {
                                "final_score": 0.0,
                                "model_score": 0.0,
                                "rule_score": 0.0,
                                "hard_fail": True,
                                "char_count": 0,
                                "query": "",
                                "rule_breakdown": {
                                    "length": 0.0,
                                    "must_include": 0.0,
                                    "banned_phrases": 0.0,
                                    "template_tone": 0.0,
                                    "formatting": 0.0,
                                    "detailfulness": 0.0,
                                    "email_shape": 0.0,
                                    "anti_repetition": 0.0,
                                },
                                "notes": [f"generation error: {exc}"],
                            },
                            "failure_tags": ["generation_error"],
                            "error": str(exc),
                        }
                    round_candidates.append(candidate)

            for heuristic_offset, (profile, candidate_text) in enumerate(heuristic_variants, start=len(round_candidates) + 1):
                round_candidates.append(
                    build_heuristic_candidate(
                        run_dir=run_dir,
                        spec=spec,
                        task=task,
                        source_text=source_text,
                        current_best_text=current_best_text,
                        current_best_score_value=current_best_score.final_score,
                        enforce_continuity=enforce_continuity,
                        round_number=round_number,
                        candidate_index=heuristic_offset,
                        profile=profile,
                        candidate_text=candidate_text,
                    ),
                )

            selected = pick_best_candidate(round_candidates)
            selected_candidates.append(selected)
            selected_score = selected["score"]
            delta = float(selected_score["final_score"]) - float(current_best_score.final_score)
            improved = (not selected_score["hard_fail"]) and delta >= margin
            force_retry_with_model = should_force_model_retry(
                session_mode=session_mode,
                round_number=round_number,
                max_rounds=max_rounds,
                round_candidates=round_candidates,
                heuristic_variants=heuristic_variants,
            )
            continue_requested, quality_gate_tags_out = should_continue_refinement(
                selected=selected,
                delta=delta,
                margin=margin,
                round_number=round_number,
                max_rounds=max_rounds,
            )
            quality_gate_exhausted = bool(quality_gate_tags_out) and not continue_requested
            if improved and continue_requested:
                decision = "continue"
                reason = "selected challenger improved, but quality gate requested another round"
                next_failure_tags = quality_gate_tags_out
                next_step = "continue"
            elif improved and quality_gate_exhausted:
                decision = "keep"
                reason = "selected challenger improved, but round budget was exhausted"
                next_failure_tags = quality_gate_tags_out
                next_step = "stop"
            elif improved:
                decision = "keep"
                reason = "selected challenger improved and passed quality gate"
                next_failure_tags = []
                next_step = "stop"
            else:
                next_failure_tags = aggregate_failure_tags(round_candidates, selected)
                retry_tags = retryable_quality_tags(next_failure_tags)
                if round_number < max_rounds and retry_tags:
                    decision = "continue"
                    reason = "selected challenger did not improve, but retryable quality issues remain"
                    next_failure_tags = retry_tags
                    quality_gate_tags_out = retry_tags
                    next_step = "continue"
                else:
                    decision = "discard"
                    reason = "selected challenger did not improve enough"
                    next_step = "stop"

            round_payload = {
                "round": round_number,
                "profiles": profiles,
                "failure_tags_in": current_failure_tags,
                "strategy_directives": directives,
                "force_model_candidates": force_model_candidates,
                "force_retry_with_model": force_retry_with_model,
                "baseline_text": current_best_text,
                "baseline_score": current_best_score.as_dict(),
                "candidates": round_candidates,
                "selected_candidate": selected,
                "delta": round(delta, 6),
                "decision": decision,
                "reason": reason,
                "quality_gate_tags": quality_gate_tags_out,
                "next_step": next_step,
                "revision_mode": revision_mode,
                "base_text_kind": base_text_kind,
            }
            session_trace.append(round_payload)

            state = evolve_after_attempts(
                state,
                task=task,
                chosen_profile=selected["profile"],
                failure_tags=next_failure_tags,
                improved=improved and not quality_gate_tags_out,
                baseline_text=current_best_text,
                challenger_text=selected["text"],
                delta=delta,
            )

            if improved:
                improved_any = True
                current_best_text = selected["text"]
                current_best_score = score_candidate(spec, current_best_text, source_text)
                final_kept_candidate = selected
                if continue_requested:
                    current_failure_tags = next_failure_tags
                    continue
                current_failure_tags = []
                break

            current_failure_tags = next_failure_tags
            if decision == "continue":
                continue
            if improved_any or round_number >= max_rounds:
                break
            if force_retry_with_model:
                continue
            if heuristic_variants and (not next_failure_tags or set(next_failure_tags).issubset({"no_improvement"})):
                break

    save_state(state)
    write_json(state_after_path, state)

    if improved_any and final_kept_candidate:
        final_candidate = final_kept_candidate
    elif selected_candidates:
        final_candidate = max(selected_candidates, key=candidate_rank_key)
    else:
        final_candidate = {
            "candidate_index": 0,
            "profile": "baseline",
            "text": initial_baseline_text,
            "candidate_path": "",
            "generation_path": "",
            "score_path": "",
            "score": baseline_score_payload,
            "failure_tags": [],
        }

    final_compare_text = current_best_text if improved_any else final_candidate["text"]
    if not final_compare_text.strip():
        final_compare_text = initial_baseline_text

    final_challenger_score = (
        current_best_score.as_dict()
        if improved_any
        else dict(final_candidate.get("score") or baseline_score_payload)
    )
    baseline_final_score = baseline_score_payload

    baseline_path = run_dir / "baseline.txt"
    challenger_path = run_dir / "challenger.txt"
    baseline_score_path = run_dir / "baseline.score.json"
    challenger_score_path = run_dir / "challenger.score.json"
    compare_path = run_dir / "compare-result.json"
    best_path = run_dir / "best.txt"
    report_md_path = run_dir / "report.md"
    report_html_path = run_dir / "report.html"

    write_text(baseline_path, initial_baseline_text)
    write_text(challenger_path, final_compare_text)
    dump_score_json(
        baseline_score_path,
        {
            "candidate_path": str(baseline_path.resolve()),
            "source_path": str(source_path.resolve()),
            "spec_path": str(spec_path.resolve()),
            **baseline_final_score,
        },
    )
    dump_score_json(
        challenger_score_path,
        {
            "candidate_path": str(challenger_path.resolve()),
            "source_path": str(source_path.resolve()),
            "spec_path": str(spec_path.resolve()),
            **final_challenger_score,
        },
    )
    compare_payload = compare_payload_local(
        spec_path=spec_path,
        source_path=source_path,
        baseline_path=baseline_path,
        challenger_path=challenger_path,
        baseline_score=baseline_final_score,
        challenger_score=final_challenger_score,
        margin=margin,
    )
    dump_score_json(compare_path, compare_payload)
    write_round_log_local(run_dir, compare_payload)
    write_text(
        best_path,
        final_compare_text if compare_payload["winner"] == "challenger" else initial_baseline_text,
    )
    report_md_text = build_markdown(compare_payload, initial_baseline_text, final_compare_text)
    write_text(report_md_path, report_md_text)
    write_text(report_html_path, build_html(compare_payload, initial_baseline_text, final_compare_text))

    session_trace_payload = {
        "session_mode": session_mode,
        "input_mode": payload["input_mode"],
        "run_budget": run_budget,
        "improved_any": improved_any,
        "state_before_path": str(state_before_path.resolve()),
        "state_after_path": str(state_after_path.resolve()),
        "rounds": session_trace,
    }
    session_trace_path = run_dir / "session-trace.json"
    session_trace_md_path = run_dir / "session-trace.md"
    write_json(session_trace_path, session_trace_payload)
    session_trace_markdown = build_trace_markdown(session_trace_payload)
    write_text(session_trace_md_path, session_trace_markdown)

    user_visible_md_path = run_dir / "user-visible.md"
    user_visible_html_path = run_dir / "user-visible.html"
    user_visible_summary = build_user_visible_summary(
        task=task,
        compare_payload=compare_payload,
        baseline_text=initial_baseline_text,
        challenger_text=final_compare_text,
        session_trace=session_trace,
        report_html_path=str(report_html_path.resolve()),
        trace_path=str(session_trace_path.resolve()),
        run_budget=run_budget,
    )
    write_text(user_visible_md_path, user_visible_summary)
    write_text(user_visible_html_path, build_user_visible_html(user_visible_summary))

    payload_out = {
        "run_dir": str(run_dir.resolve()),
        "decision": compare_payload["decision"],
        "winner": compare_payload["winner"],
        "delta": compare_payload["delta"],
        "baseline_score": compare_payload["baseline"]["final_score"],
        "challenger_score": compare_payload["challenger"]["final_score"],
        "best_path": str(best_path.resolve()),
        "compare_result": str(compare_path.resolve()),
        "markdown_report": str(report_md_path.resolve()),
        "html_report": str(report_html_path.resolve()),
        "session_mode": session_mode,
        "input_mode": payload["input_mode"],
        "parse_result": str(parse_path.resolve()),
        "user_brief": str(brief_path.resolve()),
        "llm_base_url": base_url,
        "llm_model": model,
        "llm_backend_error": generation_backend_error,
        "baseline_text": initial_baseline_text,
        "challenger_text": final_compare_text,
        "baseline_generation": str((run_dir / "baseline.generation.json").resolve()) if (run_dir / "baseline.generation.json").exists() else "",
        "challenger_generation": final_candidate.get("generation_path", ""),
        "score_summary": {
        "decision": compare_payload["decision"],
        "reason": compare_payload["reason"],
        "delta": compare_payload["delta"],
        "margin": compare_payload["margin"],
        "baseline": {
            "final_score": compare_payload["baseline"]["final_score"],
            "model_score": compare_payload["baseline"]["model_score"],
            "rule_score": compare_payload["baseline"]["rule_score"],
            "hard_fail": compare_payload["baseline"]["hard_fail"],
            "notes": compare_payload["baseline"]["notes"],
        },
        "challenger": {
            "final_score": compare_payload["challenger"]["final_score"],
            "model_score": compare_payload["challenger"]["model_score"],
            "rule_score": compare_payload["challenger"]["rule_score"],
            "hard_fail": compare_payload["challenger"]["hard_fail"],
            "notes": compare_payload["challenger"]["notes"],
        },
        },
        "report_markdown_text": report_md_text,
        "session_trace": session_trace,
        "session_trace_path": str(session_trace_path.resolve()),
        "session_trace_markdown": session_trace_markdown,
        "user_visible_summary_markdown": user_visible_summary,
        "user_visible_markdown_path": str(user_visible_md_path.resolve()),
        "user_visible_html_path": str(user_visible_html_path.resolve()),
        "strategy_state_before": str(state_before_path.resolve()),
        "strategy_state_after": str(state_after_path.resolve()),
    }
    result_json_path = run_dir / "result.json"
    write_json(result_json_path, payload_out)
    print("=== HUMANIZE_AGENT_RELAY_INSTRUCTION_BEGIN ===")
    print(
        "下面 HUMANIZE_FINAL_RESPONSE 标记中间的 markdown 是最终用户答案；"
        "请原样粘贴给用户，不要总结、不要改写、不要只给最终文案。"
    )
    print("如果输出被截断，请读取并原样返回 user-visible.md。")
    print("=== HUMANIZE_AGENT_RELAY_INSTRUCTION_END ===")
    print("=== HUMANIZE_FINAL_RESPONSE_BEGIN ===")
    print(user_visible_summary)
    print("=== HUMANIZE_FINAL_RESPONSE_END ===")


if __name__ == "__main__":
    main()


path: scripts/run_regression_suite.py
content: from __future__ import annotations

import argparse
import json
import subprocess
import sys
import time
from pathlib import Path
from typing import Any


def load_cases(path: Path) -> list[dict[str, Any]]:
    payload = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(payload, list):
        raise ValueError("regression cases must be a JSON list")
    return payload


def sanitize_case_id(value: str) -> str:
    return "".join(ch if ch.isalnum() or ch in {"-", "_"} else "-" for ch in value).strip("-") or "case"


def extract_final_response(stdout: str) -> str:
    begin = "=== HUMANIZE_FINAL_RESPONSE_BEGIN ==="
    end = "=== HUMANIZE_FINAL_RESPONSE_END ==="
    if begin not in stdout or end not in stdout:
        return ""
    return stdout.split(begin, 1)[1].split(end, 1)[0].strip()


def count_phrases(text: str, phrases: list[str]) -> dict[str, int]:
    return {phrase: text.count(phrase) for phrase in phrases}


def run_case(root: Path, python_bin: str, output_root: Path, case: dict[str, Any], timeout: int) -> dict[str, Any]:
    case_id = sanitize_case_id(str(case.get("id") or "case"))
    prompt = str(case.get("prompt") or "").strip()
    if not prompt:
        raise ValueError(f"case {case_id} is missing prompt")

    run_dir = output_root / case_id
    if run_dir.exists():
        subprocess.run(["rm", "-rf", str(run_dir)], check=True)

    cmd = [
        python_bin,
        str(root / "humanize.py"),
        "--text",
        prompt,
        "--run-dir",
        str(run_dir),
        "--output-root",
        str(output_root),
    ]
    started_at = time.time()
    proc = subprocess.run(
        cmd,
        cwd=str(root),
        capture_output=True,
        text=True,
        timeout=timeout,
    )
    elapsed = round(time.time() - started_at, 3)
    result_path = run_dir / "result.json"
    result_payload = json.loads(result_path.read_text(encoding="utf-8")) if result_path.exists() else {}
    session_trace = result_payload.get("session_trace") or []
    initial_baseline_text = str(result_payload.get("baseline_text") or "").strip()
    final_text = (result_payload.get("challenger_text") or result_payload.get("baseline_text") or "").strip()
    continue_rounds = [
        round_payload.get("round")
        for round_payload in session_trace
        if round_payload.get("decision") == "continue"
    ]
    repair_rounds = [
        round_payload.get("round")
        for round_payload in session_trace
        if round_payload.get("revision_mode") == "repair"
    ]
    revision_modes = [str(round_payload.get("revision_mode") or "rewrite") for round_payload in session_trace]
    base_text_kinds = [str(round_payload.get("base_text_kind") or "source") for round_payload in session_trace]
    best_so_far = initial_baseline_text
    continuity_ok = True
    for round_payload in session_trace:
        selected_text = str((round_payload.get("selected_candidate") or {}).get("text") or "").strip()
        if round_payload.get("revision_mode") == "repair":
            baseline_text = str(round_payload.get("baseline_text") or "").strip()
            if baseline_text != best_so_far:
                continuity_ok = False
        if round_payload.get("decision") in {"keep", "continue"} and selected_text:
            best_so_far = selected_text
    final_matches_last_improved = final_text == best_so_far if final_text else False
    tracked_phrases = list(((case.get("expectations") or {}).get("tracked_phrases")) or [])
    phrase_counts: dict[str, Any] = {}
    if tracked_phrases:
        round1_selected = ""
        if session_trace:
            round1_selected = str((session_trace[0].get("selected_candidate") or {}).get("text") or "").strip()
        phrase_counts = {
            "source": count_phrases(initial_baseline_text, tracked_phrases),
            "round1_selected": count_phrases(round1_selected, tracked_phrases),
            "final": count_phrases(final_text, tracked_phrases),
        }

    expectations = case.get("expectations") or {}
    expectation_errors: list[str] = []
    if expectations.get("requires_continue_round") and not continue_rounds:
        expectation_errors.append("expected a continue round")
    if expectations.get("requires_repair_round") and not repair_rounds:
        expectation_errors.append("expected at least one repair round")
    if expectations.get("requires_best_so_far_repair") and not continuity_ok:
        expectation_errors.append("repair round did not use current best text as baseline")
    if expectations.get("requires_final_match_last_improved") and not final_matches_last_improved:
        expectation_errors.append("final text did not preserve the last improved best-so-far")
    if expectations.get("requires_tracked_phrases_removed") and phrase_counts:
        if any(count > 0 for count in phrase_counts.get("final", {}).values()):
            expectation_errors.append("tracked residual phrases were not removed in final text")

    return {
        "id": case_id,
        "elapsed_seconds": elapsed,
        "returncode": proc.returncode,
        "run_dir": str(run_dir),
        "decision": result_payload.get("decision"),
        "winner": result_payload.get("winner"),
        "delta": result_payload.get("delta"),
        "round_count": len(session_trace),
        "continue_rounds": continue_rounds,
        "has_continue_round": bool(continue_rounds),
        "repair_rounds": repair_rounds,
        "revision_modes": revision_modes,
        "base_text_kinds": base_text_kinds,
        "best_so_far_continuity_ok": continuity_ok,
        "final_matches_last_improved": final_matches_last_improved,
        "quality_gate_blocked_rounds": [
            {
                "round": round_payload.get("round"),
                "tags": round_payload.get("quality_gate_tags") or [],
            }
            for round_payload in session_trace
            if round_payload.get("quality_gate_tags")
        ],
        "tracked_phrase_counts": phrase_counts,
        "expectation_errors": expectation_errors,
        "final_text": final_text,
        "final_response": extract_final_response(proc.stdout),
        "stderr": proc.stderr.strip(),
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Run the humanize regression sample set.")
    parser.add_argument(
        "--cases",
        type=Path,
        default=Path(__file__).resolve().parent.parent / "examples" / "regression_cases.json",
    )
    parser.add_argument(
        "--output-root",
        type=Path,
        default=Path(__file__).resolve().parent.parent / "runs" / "regression-suite",
    )
    parser.add_argument("--python-bin", default=sys.executable or "python3")
    parser.add_argument("--timeout", type=int, default=240)
    parser.add_argument("--limit", type=int, default=0)
    args = parser.parse_args()

    root = Path(__file__).resolve().parent.parent
    cases = load_cases(args.cases)
    if args.limit > 0:
        cases = cases[: args.limit]

    args.output_root.mkdir(parents=True, exist_ok=True)
    summary: list[dict[str, Any]] = []
    for case in cases:
        summary.append(
            run_case(
                root=root,
                python_bin=args.python_bin,
                output_root=args.output_root,
                case=case,
                timeout=args.timeout,
            ),
        )

    summary_path = args.output_root / "summary.json"
    summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")

    for item in summary:
        if item["expectation_errors"]:
            raise SystemExit(
                f"{item['id']} failed expectations: {'; '.join(item['expectation_errors'])}",
            )
        continue_text = ",".join(str(round_no) for round_no in item["continue_rounds"]) or "-"
        repair_text = ",".join(str(round_no) for round_no in item["repair_rounds"]) or "-"
        print(
            f"{item['id']}: decision={item['decision']} "
            f"winner={item['winner']} rounds={item['round_count']} continue={continue_text} repair={repair_text} "
            f"delta={item['delta']} elapsed={item['elapsed_seconds']}s",
        )
    print(f"summary: {summary_path}")


if __name__ == "__main__":
    main()


path: scripts/run_session.py
content: from __future__ import annotations

import argparse
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import yaml

from runtime_common import reexec_into_runtime

reexec_into_runtime()

from render_run_report import build_html, build_markdown  # noqa: E402
from scoring_core import dump_score_json, load_yaml, read_text, score_candidate  # noqa: E402


def slugify(text: str) -> str:
    compact = re.sub(r"[^\w\u4e00-\u9fff]+", "-", text.strip().lower())
    compact = re.sub(r"-{2,}", "-", compact).strip("-")
    return compact[:40] or "run"


def resolve_text(path: Path | None, inline: str | None) -> str:
    if inline is not None:
        return inline.strip()
    if path is None:
        return ""
    return read_text(path)


def create_run_dir(output_root: Path, task: str) -> Path:
    run_id = datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + slugify(task)
    run_dir = output_root / run_id
    run_dir.mkdir(parents=True, exist_ok=False)
    (run_dir / "rounds").mkdir(exist_ok=True)
    return run_dir


def compare_payload(
    spec: dict[str, Any],
    spec_path: Path,
    source_path: Path,
    baseline_path: Path,
    challenger_path: Path,
    baseline: Any,
    challenger: Any,
    margin: float,
) -> dict[str, Any]:
    delta = challenger.final_score - baseline.final_score
    if challenger.hard_fail:
        decision = "discard"
        winner = "baseline"
        reason = "challenger failed hard constraints"
    elif delta >= margin:
        decision = "keep"
        winner = "challenger"
        reason = "challenger improved beyond threshold"
    else:
        decision = "discard"
        winner = "baseline"
        reason = "improvement below threshold"

    return {
        "decision": decision,
        "winner": winner,
        "reason": reason,
        "margin": margin,
        "delta": round(delta, 6),
        "spec_path": str(spec_path.resolve()),
        "source_path": str(source_path.resolve()) if source_path.exists() else "",
        "baseline": {
            "path": str(baseline_path.resolve()),
            **baseline.as_dict(),
        },
        "challenger": {
            "path": str(challenger_path.resolve()),
            **challenger.as_dict(),
        },
    }


def write_round_log(run_dir: Path, compare: dict[str, Any]) -> None:
    payload = dict(compare)
    payload["recorded_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    log_path = run_dir / "rounds.jsonl"
    with log_path.open("a", encoding="utf-8") as fh:
        fh.write(json.dumps(payload, ensure_ascii=False) + "\n")

    rounds_dir = run_dir / "rounds"
    rounds_dir.mkdir(exist_ok=True)
    index = len(list(rounds_dir.glob("round-*.json"))) + 1
    (rounds_dir / f"round-{index:03d}.json").write_text(
        json.dumps(payload, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Create one full visible optimization run with reports.",
    )
    parser.add_argument("--spec", required=True, type=Path)
    parser.add_argument("--baseline", type=Path, default=None)
    parser.add_argument("--baseline-text", default=None)
    parser.add_argument("--challenger", type=Path, default=None)
    parser.add_argument("--challenger-text", default=None)
    parser.add_argument("--source", type=Path, default=None)
    parser.add_argument("--source-text", default=None)
    parser.add_argument("--run-dir", type=Path, default=None)
    parser.add_argument("--output-root", type=Path, default=Path("./runs"))
    parser.add_argument("--margin", type=float, default=None)
    args = parser.parse_args()

    spec = load_yaml(args.spec)
    task = str(spec.get("task", "") or "run")
    run_dir = args.run_dir.resolve() if args.run_dir else create_run_dir(args.output_root.resolve(), task)
    run_dir.mkdir(parents=True, exist_ok=True)
    (run_dir / "rounds").mkdir(exist_ok=True)

    spec_text = args.spec.read_text(encoding="utf-8")
    baseline_text = resolve_text(args.baseline, args.baseline_text)
    challenger_text = resolve_text(args.challenger, args.challenger_text)
    source_text = resolve_text(args.source, args.source_text) or baseline_text

    if not baseline_text:
        raise ValueError("A baseline draft is required")
    if not challenger_text:
        raise ValueError("A challenger draft is required")

    spec_path = run_dir / "spec.yaml"
    source_path = run_dir / "source.txt"
    baseline_path = run_dir / "baseline.txt"
    challenger_path = run_dir / "challenger.txt"
    baseline_score_path = run_dir / "baseline.score.json"
    challenger_score_path = run_dir / "challenger.score.json"
    compare_path = run_dir / "compare-result.json"
    best_path = run_dir / "best.txt"

    spec_path.write_text(spec_text, encoding="utf-8")
    source_path.write_text(source_text, encoding="utf-8")
    baseline_path.write_text(baseline_text, encoding="utf-8")
    challenger_path.write_text(challenger_text, encoding="utf-8")

    baseline_score = score_candidate(spec, baseline_text, source_text)
    challenger_score = score_candidate(spec, challenger_text, source_text)
    dump_score_json(
        baseline_score_path,
        {
            "candidate_path": str(baseline_path.resolve()),
            "source_path": str(source_path.resolve()),
            "spec_path": str(spec_path.resolve()),
            **baseline_score.as_dict(),
        },
    )
    dump_score_json(
        challenger_score_path,
        {
            "candidate_path": str(challenger_path.resolve()),
            "source_path": str(source_path.resolve()),
            "spec_path": str(spec_path.resolve()),
            **challenger_score.as_dict(),
        },
    )

    default_margin = (
        ((spec.get("evaluator") or {}).get("minimum_improvement")) or 0.015
    )
    margin = args.margin if args.margin is not None else float(default_margin)
    compare = compare_payload(
        spec,
        spec_path,
        source_path,
        baseline_path,
        challenger_path,
        baseline_score,
        challenger_score,
        margin,
    )
    dump_score_json(compare_path, compare)
    write_round_log(run_dir, compare)

    if compare["winner"] == "challenger":
        best_path.write_text(challenger_text, encoding="utf-8")
    else:
        best_path.write_text(baseline_text, encoding="utf-8")

    report_md = run_dir / "report.md"
    report_html = run_dir / "report.html"
    report_md.write_text(
        build_markdown(compare, baseline_text, challenger_text),
        encoding="utf-8",
    )
    report_html.write_text(
        build_html(compare, baseline_text, challenger_text),
        encoding="utf-8",
    )

    print(
        json.dumps(
            {
                "run_dir": str(run_dir),
                "decision": compare["decision"],
                "winner": compare["winner"],
                "delta": compare["delta"],
                "baseline_score": baseline_score.final_score,
                "challenger_score": challenger_score.final_score,
                "best_path": str(best_path),
                "compare_result": str(compare_path),
                "markdown_report": str(report_md),
                "html_report": str(report_html),
            },
            ensure_ascii=False,
            indent=2,
        ),
    )


if __name__ == "__main__":
    main()


path: scripts/runtime_common.py
content: from __future__ import annotations

import json
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Iterable

SKILL_NAME = "humanize"
DEFAULT_MODEL_ID = "BAAI/bge-reranker-v2-m3"
RUNTIME_ACTIVE_ENV = "HUMANIZE_RUNTIME_ACTIVE"
DEFAULT_GOAL = (
    "更像真人自然发送的中文沟通消息，减少模板腔、客服腔、公告腔和过度AI润色感。"
    "保持清楚、可信、有分寸。"
)


def skill_root() -> Path:
    return Path(__file__).resolve().parent.parent


def copaw_working_dir() -> Path:
    return Path(
        os.environ.get("COPAW_WORKING_DIR", "~/.copaw"),
    ).expanduser().resolve()


def runtime_root() -> Path:
    return copaw_working_dir() / "models" / SKILL_NAME


def venv_dir() -> Path:
    return runtime_root() / "runtime-venv"


def _venv_bin_dir() -> Path:
    if os.name == "nt":
        return venv_dir() / "Scripts"
    return venv_dir() / "bin"


def runtime_python() -> Path:
    if os.name == "nt":
        return _venv_bin_dir() / "python.exe"
    return _venv_bin_dir() / "python"


def model_dir() -> Path:
    return runtime_root() / "models" / "primary"


def hf_cache_dir() -> Path:
    return runtime_root() / "hf-cache"


def host_python_candidates() -> list[str]:
    candidates: list[str] = []
    wd = copaw_working_dir()
    if os.name == "nt":
        copaw_python = wd / "venv" / "Scripts" / "python.exe"
    else:
        copaw_python = wd / "venv" / "bin" / "python"
    if copaw_python.exists():
        candidates.append(str(copaw_python))
    candidates.append(sys.executable)
    for name in ("python3.12", "python3.11", "python3.10", "python3", "python"):
        resolved = shutil.which(name)
        if resolved:
            candidates.append(resolved)
    unique: list[str] = []
    seen = set()
    for item in candidates:
        if item and item not in seen:
            seen.add(item)
            unique.append(item)
    return unique


def pick_host_python() -> str:
    for candidate in host_python_candidates():
        try:
            out = subprocess.check_output(
                [
                    candidate,
                    "-c",
                    (
                        "import sys, json; "
                        "print(json.dumps({'major': sys.version_info[0], "
                        "'minor': sys.version_info[1]}))"
                    ),
                ],
                text=True,
            ).strip()
            info = json.loads(out)
            if (info["major"], info["minor"]) >= (3, 9):
                return candidate
        except Exception:
            continue
    raise RuntimeError("No usable Python >= 3.9 interpreter found for bootstrap")


def pip_install_args() -> list[str]:
    return [
        "-m",
        "pip",
        "install",
        "--disable-pip-version-check",
        "--upgrade",
        "pip",
        "setuptools",
        "wheel",
    ]


def _subprocess_env(extra: dict[str, str] | None = None) -> dict[str, str]:
    env = os.environ.copy()
    env.setdefault("HF_HOME", str(hf_cache_dir()))
    env.pop("TRANSFORMERS_CACHE", None)
    env.setdefault("HF_HUB_DISABLE_XET", "1")
    env.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
    env.setdefault("TOKENIZERS_PARALLELISM", "false")
    if extra:
        env.update(extra)
    return env


def ensure_runtime(force_reinstall: bool = False) -> None:
    rt_root = runtime_root()
    rt_root.mkdir(parents=True, exist_ok=True)
    hf_cache_dir().mkdir(parents=True, exist_ok=True)
    if force_reinstall and venv_dir().exists():
        shutil.rmtree(venv_dir())
    if runtime_python().exists():
        return
    host_python = pick_host_python()
    subprocess.run(
        [host_python, "-m", "venv", str(venv_dir())],
        check=True,
    )
    subprocess.run(
        [str(runtime_python()), *pip_install_args()],
        check=True,
        env=_subprocess_env(),
    )
    subprocess.run(
        [
            str(runtime_python()),
            "-m",
            "pip",
            "install",
            "--disable-pip-version-check",
            "-r",
            str(skill_root() / "requirements.lock.txt"),
        ],
        check=True,
        env=_subprocess_env(),
    )


def is_runtime_process() -> bool:
    return os.environ.get(RUNTIME_ACTIVE_ENV) == "1"


def reexec_into_runtime(force_reinstall: bool = False) -> None:
    if is_runtime_process() and Path(sys.executable).resolve() == runtime_python().resolve():
        return
    ensure_runtime(force_reinstall=force_reinstall)
    env = _subprocess_env(
        {
            RUNTIME_ACTIVE_ENV: "1",
            "ZH_COMM_EVOLVER_RUNTIME_ROOT": str(runtime_root()),
        },
    )
    result = subprocess.run(
        [str(runtime_python()), *sys.argv],
        env=env,
    )
    raise SystemExit(result.returncode)


def download_model_snapshot(
    model_id: str = DEFAULT_MODEL_ID,
    force: bool = False,
) -> Path:
    from huggingface_hub import snapshot_download

    target_dir = model_dir()
    if force and target_dir.exists():
        shutil.rmtree(target_dir)
    target_dir.mkdir(parents=True, exist_ok=True)
    marker = target_dir / ".download-complete.json"
    if marker.exists():
        return target_dir

    snapshot_download(
        repo_id=model_id,
        local_dir=str(target_dir),
        allow_patterns=[
            "*.json",
            "*.txt",
            "*.model",
            "*.py",
            "*.safetensors",
            "tokenizer*",
            "sentencepiece*",
            "special_tokens_map*",
            "vocab*",
        ],
    )
    marker.write_text(
        json.dumps({"model_id": model_id}, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    return target_dir


def runtime_summary() -> dict[str, str]:
    return {
        "skill_name": SKILL_NAME,
        "model_id": DEFAULT_MODEL_ID,
        "copaw_working_dir": str(copaw_working_dir()),
        "runtime_root": str(runtime_root()),
        "venv_python": str(runtime_python()),
        "model_dir": str(model_dir()),
        "platform": platform.platform(),
    }


def write_json(path: Path, payload: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        json.dumps(payload, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )


def run_command(command: Iterable[str], *, env: dict[str, str] | None = None) -> None:
    subprocess.run(list(command), check=True, env=_subprocess_env(env))


path: scripts/score_copy.py
content: from __future__ import annotations

import argparse
import json
from pathlib import Path

from runtime_common import reexec_into_runtime

reexec_into_runtime()

from scoring_core import dump_score_json, load_yaml, read_text, score_candidate  # noqa: E402


def main() -> None:
    parser = argparse.ArgumentParser(description="Score one candidate draft.")
    parser.add_argument("--spec", required=True, type=Path)
    parser.add_argument("--candidate", required=True, type=Path)
    parser.add_argument("--source", type=Path, default=None)
    parser.add_argument(
        "--output",
        type=Path,
        default=None,
        help="Optional JSON output path. Defaults to <candidate>.score.json",
    )
    args = parser.parse_args()

    spec = load_yaml(args.spec)
    candidate_text = read_text(args.candidate)
    source_text = read_text(args.source)
    score = score_candidate(spec, candidate_text, source_text)

    payload = {
        "candidate_path": str(args.candidate.resolve()),
        "source_path": str(args.source.resolve()) if args.source else "",
        "spec_path": str(args.spec.resolve()),
        **score.as_dict(),
    }
    output_path = args.output or args.candidate.with_suffix(".score.json")
    dump_score_json(output_path, payload)
    print(json.dumps(payload, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


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