# Job Hunt

> 求职猎手主入口。上传招聘平台岗位详情页截图（Boss直聘、智联招聘、前程无忧等均可），与你的简历做 STAR 匹配分析，生成定制简历和开场白，按匹配度排序产出 shortlist。支持子命令：fetch / analyze / tailor / status / clean。

- Skill: `jpcwhj/job-hunt` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add jpcwhj/job-hunt`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jpcwhj/job-hunt/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: jpcwhj (https://skillmd.com/u/jpcwhj)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jpcwhj/job-hunt

---


# job-hunt 求职猎手

## ⚡ 全流程自动执行约束（最高优先级，全程有效）

**仅适用于 `/job-hunt` 无参数的全流程模式。**

用户提供简历 + 截图确认后，**剩余所有步骤全部自动连续执行，直到输出 shortlist**。

- **Step 3 fetcher 返回 → Step 7 之间：严禁输出任何文字**。输出文字 = turn 结束 = 等待用户 = 流程中断
- 每一步结束后，立即调用下一步所需的工具（Skill / Bash / Read / Write），不插入任何文字
- 子 skill（fetcher / analyzer / tailor）的返回结果是内部数据，**不得回显、不得复述、不得向用户展开汇报，直接用于下一步**
- **禁止**在步骤之间询问用户「是否继续」「要不要开始下一步」

违反上述约束 = 流程中断，用户体验完全崩溃。

---

## 子命令路由

根据用户输入判断执行路径：

| 用户输入 | 执行 |
|---|---|
| `/job-hunt` 无参数 | 全流程（Step 1→7） |
| `/job-hunt fetch` | 只执行截图导入（Step 1+2+3） |
| `/job-hunt analyze` | 只执行分析（Step 1+4） |
| `/job-hunt tailor` | 执行排序（Step 5）+ 定制（Step 6） |
| `/job-hunt status` | 输出当前 run 状态（Step 7b） |
| `/job-hunt clean` | 强制清理所有缓存和产物（Step 8） |

---

## Step 1：确定工作目录与初始化

`work_dir` = Claude 启动时所在的当前目录（`./` 的绝对路径）。

用 Bash 获取：`pwd`，将结果作为 `work_dir`。

`data_dir` = `<data_dir>/jobHuntSkillData`（所有数据文件统一放在这一层目录下，不直接写入 `work_dir`）。

确保以下目录存在（用 Bash `mkdir -p` 创建）：
```
<data_dir>/.work/jd-pool/
<data_dir>/output/
```

生成 `run_id`（格式：`YYYY-MM-DD-HHMM`，使用当前本地时间）。
创建目录 `<data_dir>/output/<run_id>/`。

初始化 `<data_dir>/output/<run_id>/state.json`：
```json
{
  "run_id": "<run_id>",
  "phase": "init",
  "stages": {
    "fetched": [],
    "analyzed": [],
    "analysis_errors": [],
    "tailored": []
  },
  "last_error": null,
  "checkpoint_at": "<当前 ISO 8601 时间>"
}
```

**后续所有步骤中凡涉及路径的地方，一律使用 `data_dir` 代替 `work_dir`**。传给子 skill 的 `work_dir` 参数也传入 `data_dir` 的值。

**子命令特殊处理**：若当前子命令为 `analyze`、`tailor` 或 `status`，在生成新 run_id 之前，先扫描 `<data_dir>/output/` 下已有的 run 目录（格式 `YYYY-MM-DD-HHMM`），若存在则**复用最新一个 run_id**（不创建新目录，读取已有 state.json 继续使用）；若不存在，则按上述流程创建新 run_id。

---

## Step 2：获取简历

（全流程、fetch、analyze 子命令时执行）

检查 `<data_dir>/.work/resume.md` 是否存在：

**若已存在**：告知用户：

「已检测到上次保存的简历，继续使用。
如需更换简历，直接把新简历发给我（支持文件 / 路径 / 粘贴文本），我会帮你替换并重新评估。
如果继续用当前简历，告诉我「继续」。」

停止执行，等待用户回复：
- 用户发来新简历 → 按下方「若不存在」的方式处理，覆盖保存 `resume.md`，**进入 Step 2.5 重新评估**
- 用户说「继续」或其他非简历内容 → 跳过本步骤，跳过 Step 2.5，直接进入 Step 3

**若不存在**：提示用户提供简历：

「请提供你的简历，支持以下两种方式：
① 发送 `.md` 格式简历文件，或告诉我文件的本地路径（如 `/Users/xxx/resume.md`）
② **直接将简历文字粘贴到消息框发送**（最快最稳，从任意 PDF/Word 阅读器全选复制即可）

不支持直接上传 PDF/Word 等版式文件——这类格式跨环境解析容易失真或失败。请用方式 ② 直接贴文本。」

根据用户提供方式处理：
- **发文件（.md）/ 文件路径（.md）**：用 Read 工具或 Bash 读取文件内容
- **发文件 / 文件路径（其他格式，如 .pdf / .docx / .doc / .txt / .rtf 等）**：**不要尝试任何形式的解析**，直接告知用户：

  「暂不支持 PDF / Word 等版式文件上传（跨环境解析容易失真）。请改用：
  ① 用任意 PDF/Word 阅读器打开简历，全选（Cmd/Ctrl+A）→ 复制（Cmd/Ctrl+C）→ 粘贴到这里发给我
  ② 或将简历另存为 `.md` 格式后重发」

  停止执行，等待用户重新操作

- **粘贴文本**：按下方「粘贴文本处理流程」执行

⛔ **伦理硬约束**：无论用户提供什么格式，**绝对禁止凭空编造、想象、推断简历内容**。识别不了的格式就停止流程让用户重传，绝不生成假简历。

**.md 文件分支处理**：读取内容后直接写入 `<data_dir>/.work/resume.md`，告知用户「✅ 简历已保存。」，进入 Step 2.5。

---

### 粘贴文本处理流程

仅当用户走「粘贴文本」分支时执行：

#### 步骤 1：保存原始留底

将用户粘贴的**原始内容**（一字不改）写入 `<data_dir>/.work/resume.raw.md`。这是任何后续清洗的回滚备份。

#### 步骤 2：启发式检测格式是否已 OK

判断粘贴文本是否「已是结构化 Markdown」，必须**同时**满足以下条件：

- 包含 ≥ 2 处章节标记（行首是 `#`、`##` 或 `1.` / `2.` 这类有序列表标号）
- 包含 ≥ 2 处列表标记（行首是 `-` 或 `*` 或 `+` 后接空格）
- 平均非空行长度 ≥ 15 个字符（说明没被一行一词强行断开）
- 不存在大量"单字孤立行"（连续 5 行以上每行只有 1-3 字符的情况）

**全部满足**：直接把原始内容写入 `<data_dir>/.work/resume.md`，告知用户「✅ 简历已保存（格式良好，直接使用）。」，进入 Step 2.5。**不询问、不清洗**。

**任一不满足**：进入步骤 3。

#### 步骤 3：询问用户选择

**关键约束：**

1. **必须输出用户粘贴的完整简历原文**（一字不少，不是预览、不是摘要、不是前 N 字截断）
2. 简历原文之后才是 A/B 问题
3. A/B 问题必须是整条消息的**最后一段**，让用户视线最后落点就是 `👉 回复 A 或 B`
4. 简历原文之前**不要**写任何说明文字（如"以下是你的简历""收到了"等开场白）
5. 简历原文用代码块（``` 包裹）显示，避免 Markdown 渲染干扰原始格式

输出格式严格如下：

````
```
<用户粘贴的简历完整原文，一字不动>
```

---

📝 上面是你粘贴的简历内容。看起来格式有些问题——可能存在断行、缺章节标记等，常见于从 PDF/Word 复制带过来的。

两种处理方式：

A. **帮我整理一下** —— 只动格式：合并被断开的同段、还原项目符号、加章节标题；**不动一个字**
B. **保留我的原始格式** —— 我就要这样，不要改

👉 回复 A 或 B
````

停止执行，等待用户回复。

**禁止**：
- 截断简历内容（即使简历很长也要全部贴出）
- 在 A/B 问题之后再追加任何文字
- 在简历原文上方加任何标题/说明（直接代码块开始）

#### 步骤 4：按用户选择处理

- **用户回 A**（或语义等价：「整理」「清洗」「帮我弄」「随便」等）→ 进入步骤 5 做结构归一化
- **用户回 B**（或语义等价：「不动」「原样」「保留」「不要改」等）→ 把原始内容写入 `<data_dir>/.work/resume.md`，告知用户「✅ 已按你的原始格式保存。」，进入 Step 2.5
- **用户回模糊内容**（如「？」「啥意思」）→ 简短重新解释一次 A/B 含义，再等回复
- **用户直接发了新简历**→ 当作重新粘贴处理，回到步骤 1

#### 步骤 5：执行结构归一化（仅 A 路径）

**严格只动格式，不动一个字**。允许做和绝对禁止做的清单：

✅ **允许做**：

| 操作 | 说明 |
|---|---|
| 合并被强行断开的同句行 | `负责抖音\n小视频\n前端开发` → `负责抖音小视频前端开发`。判断标准：上一行不以句末标点结尾（`。！？.!?`）且下一行不像新章节/列表项的开始 |
| 合并被拆散的标题/姓名 | `张\n三` → `张三`；`工作经\n历` → `工作经历` |
| 移除连续空行 | 3 个及以上连续空行 → 保留 1 个 |
| 还原项目符号 | 段首孤立的 `·` `●` `▪` `◦` `▶` → 替换为 `- ` |
| 还原章节标题层级 | 识别"个人信息""工作经历""项目经历""项目经验""教育背景""专业技能""自我评价"等常见章节关键词，在前面加 `## ` |
| 规范缩进 | tab 字符 → 2 空格；行首过多空格统一为 2 空格倍数 |
| 修复编码异常 | 删除零宽字符 `​` `­` 等不可见字符 |
| 删除明显多栏混排导致的乱序片段 | 仅当能 100% 确定是多栏问题且能正确还原时；不确定就不要动 |

❌ **绝对禁止**：

- 增加、删除、改写任何文字内容（哪怕一个字）
- 修正错别字（用户写「杭洲」就保留「杭洲」）
- 翻译、总结、精简、扩写
- 根据上下文推断补齐缺失信息
- 添加用户没提的章节、技能、经历
- 改变事件顺序、时间、数字

**违反任何一条 ❌ 即触发伦理硬约束告警，必须停止并告知用户。**

清洗完成后：
- 把清洗结果写入 `<data_dir>/.work/resume.md`
- 简短告知用户清洗量：「📝 已整理简历格式：合并 N 处断行 / 还原 M 个项目符号 / 加 K 个章节标题。原始版本已留底（如不满意可说"还原原版"）。」
- 进入 Step 2.5

---

### 用户事后反悔（任何阶段）

当用户在后续流程中说出「**还原原版**」「**用原版**」「**用我贴的那版**」「**不要清洗后的**」等语义等价指令时：

1. 检查 `<data_dir>/.work/resume.raw.md` 是否存在
   - 不存在 → 告知「未找到原始留底版本（可能本次简历没经过清洗）。如需更换，直接重新粘贴或发送新简历。」
   - 存在 → 用 Bash 把 `resume.raw.md` 复制覆盖到 `resume.md`
2. 告知用户「✅ 已切回原始版本。」
3. 重新进入 Step 2.5 评估

---

## Step 2.5：简历质量评估

（仅在用户本次会话中新提供了简历时执行；复用缓存的直接跳到 Step 3）

读取 `<data_dir>/.work/resume.md`，找出所有**可评估单元**，逐一打分，输出结果。

⛔ **必须先过滤，再评估。以下三条规则是强制先决条件，不允许跳过或绕过：**
- 在任何评估行为之前，必须完成规则一→二→三的完整筛选流程
- **不得根据公司名、职位名、部门名推断该段经历的工作内容进行评估**——评估对象只能是简历中白纸黑字写出的行动动词和结果描述
- 只有通过规则三的文字内容，才能出现在「逐段分析」中；未通过筛选的区块/行，一律不出现

**三条判断规则，按顺序执行：**

---

**规则一：整个区块跳过（不看内容，直接略过）**

下列区块不论写了什么，整体跳过，不评估：
- 个人信息 / 基本信息 / 联系方式
- 专业技能 / 技能特长 / 技术能力 / 工具使用 / 技术栈
- 教育背景 / 教育经历 / 学历
- 自我评价 / 个人简介 / 求职意向
- 证书 / 奖项 / 荣誉

**区块名不限于以上文字**，只要语义属于上述类型（如「我的技能」「掌握的工具」「所获奖励」），同样整体跳过。

⚠️ **合并区块的处理**：如果区块名同时包含工作和教育（如「工作及教育经历」「教育与工作背景」），**不整体跳过**，进入规则二逐行判断。其中属于教育条目的行（含学校名、专业名、学位、在校时间）按规则二同样会被跳过。

**其他所有区块**（工作经历、项目经历、实习经历、创业经历、兼职经历，或任何自定义名称的经历类区块）进入规则二。

---

**规则二：区块内的标题行跳过**

经历类区块内，每一行单独判断。如果这一行**同时满足以下两点**，跳过：
- 没有行动动词（负责、主导、设计、推动、搭建、优化、完成、实现、带领等）
- 没有结果描述（提升、降低、增长、减少、达到、超过、节省，或具体数字/百分比）

⚠️ **重要：职位名、部门名中含有的动词性词语不算行动动词。** 例如"运营组""品牌部""设计科""管理岗"——这些是组织单元的名称标签，不是对个人行为的描述，不触发评估。判断依据是：这个词是在描述「这个人做了什么」，还是在说明「这个部门/岗位叫什么名字」。

例如：
- 「字节跳动 · 产品经理 · 2021.03—2023.06」→ ❌ 跳过（无动词无结果）
- 「某文化传媒公司　2023.7~2025.11　新媒体-运营组」→ ❌ 跳过（"运营"是部门名，不是行动描述）
- 「某互联网科技公司　2022.6~2023.6　市场部-品牌组」→ ❌ 跳过（"品牌"是部门名，不是行动描述）
- 「xx大学　2018.9~2022.6　新闻传播专业-本科」→ ❌ 跳过（教育条目，无行动无结果）
- 「项目名称：xxx 平台 | 角色：负责人 | 2022.01—2022.06」→ ❌ 跳过（无动词无结果）

---

**⚠️ 空区块规则（规则二执行完后立即检查）**

若某经历类区块经规则二处理后，**全部行均被跳过**（即该区块内无任何行含有行动动词或结果描述），则该区块**不进入下方三维度打分**，**不得为该区块生成任何评估条目，也不得从其他区块借调内容来填充**。

典型场景：「工作及教育经历」区块内只有公司名+时间+部门名三行头部信息，规则二全部跳过后，该区块不参与逐段打分。

**📛 但空壳的工作/实习经历不能静默消失，必须显著警示：**

在跳过这类区块时，记录其中属于**工作/实习经历**的空壳条目（公司名 + 时间段，例如「某文化传媒公司 2023.7~2025.11」）。只要存在至少一条，就必须在评估输出**最顶部**输出「空壳经历警示」块（见下方输出格式）。

原因：空壳工作经历是简历最致命的短板，且 tailor 阶段**无法**为它做任何改写（伦理红线禁止编造内容）。若这些空壳覆盖了用户的主要工作年限，补全它们比任何简历定制都重要——必须让用户清楚看到，而不是让它默默消失。教育条目空壳不在此警示范围（教育经历本就无需行动/结果）。

---

**规则三：剩下的内容才评估**

通过前两条规则筛选后剩下的句子/条目，即为**可评估单元**，进入下方三维度打分。

⚠️ 标题行与描述混写在同一行时，按规则二判断：只要含有行动动词或结果描述，就评估。
- 「在字节跳动担任产品经理期间主导了 xx 项目落地，DAU 提升 40%」→ ✅ 评估（含动词+结果）

### 评估维度

⛔ **只有通过上方规则三筛选的内容才能进入此步骤。** 若某区块（如「工作及教育经历」）内所有行均被规则二跳过，该区块整体不生成任何评估条目——即使你能判断那是一段工作经历，也不评估，因为简历里没有写任何行动或结果。

每段描述内容按以下三个维度打分（✅ 合格 / ⚠️ 薄弱 / ❌ 缺失）：

| 维度 | ✅ 合格 | ⚠️ 薄弱 | ❌ 缺失 |
|---|---|---|---|
| **场景/问题**（S/P） | 有明确业务背景或要解决的问题 | 背景模糊，一笔带过 | 无任何场景描述 |
| **行动**（A） | 具体描述「我做了什么」，有方法/手段 | 只写职责，没有行动细节 | 缺失 |
| **结果**（R） | 有量化数字，或明确的业务价值 | 结果模糊（如"效果不错"） | 无结果描述 |

额外检查：
- 是否大量使用「负责」「参与」「协助」等被动词（减分项，建议改为主动动词）
- 是否有明显可量化但留白的指标（tailor 阶段会插入 `[请填写：xxx]` 占位）

### 输出格式

```
📋 简历质量评估

<【仅当检测到空壳工作/实习经历时，在最顶部输出此块；否则整块省略】
⚠️ 重要提醒：你有 <N> 段工作经历是空的
  · <公司名> · <时间段>
  · <公司名> · <时间段>
这几段只有公司名和时间，没写任何工作内容和成果。这是你简历最大的短板：
  - 系统算匹配度时，这些经历因为没内容会被整体跳过，匹配分会明显偏低
  - 定制简历时，AI 不会替你编造内容（伦理红线），这几段只能原样保留、无法优化
👉 补全这几段的「做了什么 + 拿到什么结果」，比任何简历定制都重要。强烈建议先选 A 补这部分。
>

整体：<一句话总结，如"行动描述较充分，但结果量化普遍缺失，建议优先补充">

逐段分析：

【公司名 · 职位名】
  场景/问题 ✅  行动 ⚠️  结果 ❌
  问题：<具体说明薄弱点>
  建议：<一句话改写方向>

【项目名 · 角色】
  场景/问题 ⚠️  行动 ✅  结果 ⚠️
  问题：<说明>
  建议：<建议>

（…其他段落）

⚡ 最值得优先改的 1-2 件事：
1. <最高优先级>
2. <次优先级>

🔧 专业技能板块诊断：

<独立于上方 STAR 评估，专项检查技能板块（语义识别板块，任意名称均适用；若无标题，按内容特征识别：连续排列的技能词/工具名/框架名）。按以下四类问题逐一检查，有问题输出具体条目，无问题省略该类；若整个技能板块无任何问题，省略「🔧 专业技能板块诊断」整节：>

- **虚浮词/空泛声称**：「精通」「熟练掌握」后面没有具体场景或工具，或「精通所有前端技术」这类大而无当的表述。
  针对每条有问题的条目给出**改写示范**，例：
  「精通 Python」→「熟练使用 Python（Flask/pandas，用于 xx 数据处理）」

- **缺工具名/过于宽泛**：只写了能力方向，没有写具体工具/平台/框架。例「熟悉数据分析」「了解前端开发」。
  针对每条给出建议补全方向，例：
  「熟悉数据分析」→「熟悉数据分析（SQL / Tableau / Power BI）」

- **无程度分层**：全部条目都是同一程度词（全是「熟悉」或全是「精通」），无法体现能力深浅。
  如有此问题，提示按「熟练 / 熟悉 / 了解」或用户实际情况重新分级。

- **与目标岗位明显偏差**：<此项留空，tailor 阶段结合具体 JD 再给建议；Step 2.5 无 JD 上下文，不评估此项>

<仅当上述四类至少有一类有实质问题时，才输出「🔧 专业技能板块诊断」节；若技能板块写得合理（程度词有分层、有工具名、无大量空泛声称），整节省略，不输出任何占位文字。>

---
如何继续？
A. 我自己去改简历，改完发给你重新评估
B. 先不改，用当前简历继续
```

### 用户选择处理

**选 A（修改简历）**：
- 告知用户：「好的，修改完后直接把新简历发给我（支持文件 / 路径 / 粘贴文本）。」
- 停止执行，等待用户重新发送简历
- 收到后按 Step 2 同样方式处理，覆盖保存 `resume.md`
- **重新执行 Step 2.5**（循环，直到用户选 B）

**选 B（继续）**：
- 告知用户：「好的，继续。」
- 执行 Step 3

---

## Step 3：岗位收集与导入

（全流程、fetch 子命令时执行）

提示用户：

「请上传你感兴趣的岗位，支持以下方式：

【方式一】直接发岗位详情页截图（任意招聘平台，Boss/智联/猎聘/拉勾等）
          一次可发多张，📌 一张截图 = 一个岗位
          岗位信息长请用长截图，截图里至少要有岗位名（公司名可选）

【方式二】告诉我截图所在的目录路径（如 /Users/xxx/Desktop/jobs）
          我会自动读取该目录下的所有图片，以及 .jobs.json 文件（不递归子目录）

【方式三】用浏览器插件导出 .jobs.json 文件后一键拖进来（支持 Boss直聘 / 前程无忧 / 智联招聘 / 猎聘）
          👉 没装插件？查看 README 安装说明」

发出上方提示后，停止执行，等待用户发送截图或消息。不得提前进入循环。

**判断用户输入类型：**

| 输入 | 处理分支 |
|---|---|
| 图片附件（一张或多张） | 分支 A：截图解析 |
| 文件路径，以 `.jobs.json` 结尾 | 分支 C：JSON 导入 |
| 目录路径 | 分支 B：扫描目录（图片 + .jobs.json 混合） |

### 分支 A：截图解析

调用 Skill 工具，加载 `job-hunt-fetcher` skill，传入：
- `work_dir`：<data_dir 的值>
- `run_id`：<当前 run_id>
- `screenshots`：<本批次截图>

fetcher 内部处理（含分组确认交互），完成后写入 jd-pool。

### 分支 B：扫描目录

用 Bash 验证目录存在：
```
ls "<目录路径>"
```
若不存在，告知用户「目录不存在，请确认路径后重新发送。」并重新等待。

用 Bash 列出该目录下的图片文件和 .jobs.json 文件（只读当前目录，不递归子目录）：
```
find "<目录>" -maxdepth 1 -type f \( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.webp" -o -iname "*.jobs.json" \) | sort
```

若结果为空，告知用户「该目录下没有找到图片或 .jobs.json 文件，请确认后重新发送。」并重新等待。

告知用户找到了哪些文件：「找到 <N> 张截图 + <M> 个 .jobs.json 文件，开始处理。」

按文件类型分别处理：
- 图片 → 分支 A（调 fetcher）
- 每个 .jobs.json → 分支 C（调 import_jobs.py）

### 分支 C：JSON 导入

用 Bash 调用 import_jobs.py：

```bash
if command -v python3 >/dev/null 2>&1; then PYBIN=python3; \
elif command -v python >/dev/null 2>&1; then PYBIN=python; \
else echo "PYTHON_MISSING"; exit 1; fi

"$PYBIN" "$HOME/.claude/skills/job-hunt/import_jobs.py" "<data_dir>" "<json_path>"
```

预期 stdout：
- `OK: 已导入 N 个岗位 -> ...` → 成功，继续
- `ERROR: ...` → 告知用户「.jobs.json 处理失败：<原因>。请检查文件后重发。」并重新等待
- `PYTHON_MISSING` → 告知用户「需要 Python 3 才能处理 .jobs.json，请安装后重试。」并停止

### 公共后续

不论分支 A/B/C，全部处理完成后：

用 Bash 工具扫描 `<data_dir>/.work/jd-pool/` 下所有 `.md` 文件（排除 `.analysis.md`），读取 frontmatter 中 `run_id` 等于当前 run_id 的文件（extension-import 也包含），提取其 `id` 字段，作为本批次 ID 列表；将 ID 列表追加到 `state.json` 的 `stages.fetched`，更新 `checkpoint_at` 和 `phase` 为 `"fetched"`。

若子命令为 `fetch`：告知用户「✅ JD 导入完成，共 <N> 个岗位。运行 /job-hunt analyze 开始分析。」并停止。

**【全流程】⚠️ 本步骤完成后到 Step 7 之间，严禁输出任何文字（包括不得回显或复述子 skill 的输出内容）。state.json 更新完毕后，下一个动作必须是 Skill 工具调用（执行 Step 4），不得有任何文字夹在中间。**

---

## Step 4：分析（job-hunt-analyzer）

（全流程、analyze 子命令时执行）

确定待分析 JD 列表：
- 扫描 `<data_dir>/.work/jd-pool/` 下所有 `.md` 文件（排除 `.analysis.md` 结尾的文件）
- 读取每个文件的 frontmatter，筛选 `status.analyzed: false` 的文件，提取其 `id` 字段
- 排除 `state.json.stages.analysis_errors` 中已记录失败的 ID

若列表为空，告知用户「jd-pool 中没有待分析的 JD，请先上传截图。」并停止。

调用 Skill 工具，加载 `job-hunt-analyzer` skill，传入：
- `work_dir`：<绝对路径>
- `resume_path`：`<data_dir>/.work/resume.md`
- `jd_ids`：<待分析 JD ID 列表>
- `preferences`：`{"soft_preferences": {"prefer_industries": [], "avoid_industries": [], "prefer_company_size": []}, "ranking": {"match_weight": 1.0, "preference_weight": 0.0}}`
- `run_id`：<当前 run_id>

analyzer 返回后，用 Bash 工具更新 state.json `phase` 为 `"analyzed"`。

**【全流程】⚠️ 不得回显或复述 analyzer 的返回内容，state.json 更新完毕后立即执行 Step 5，不得有任何文字输出。**

---

## Step 5：排序

（全流程或 tailor 子命令前执行，不调用 LLM）

⚠️ **本步骤全程使用 Bash / Read 工具操作，严禁输出任何文字（包括排序过程、中间结果、排名列表）。**

用 Bash 读取 `<data_dir>/.work/jd-pool/*.analysis.md`，提取每个文件中的 `scores.total` 字段，按降序排列，得到 JD ID 有序列表。**所有已分析 JD 全部参与排序，不截断。**

用 Bash 将排序结果写入 `state.json` 的 `stages.sorted_ids` 字段，同时保留在内存中供 Step 6 直接使用。Step 6 断点续跑时，若内存中无排序结果，从 `state.json.stages.sorted_ids` 读取。

**【全流程】⚠️ state.json 更新完毕后立即调用 Skill 工具执行 Step 6，不得有任何文字输出。**

---

## Step 6：定制简历（job-hunt-tailor）

（全流程或 tailor 子命令时执行）

检查 `<data_dir>/.work/resume.md` 是否存在，若不存在则执行 Step 2 获取简历流程后再继续。

取 Step 5 排序后的完整 JD ID 列表。
排除 state.json 中已在 `stages.tailored` 的（断点续跑时跳过）。

调用 Skill 工具，加载 `job-hunt-tailor` skill，传入：
- `work_dir`：<绝对路径>
- `resume_path`：`<data_dir>/.work/resume.md`
- `jd_ids`：<完整排序后的 JD ID 列表>
- `run_id`：<当前 run_id>

tailor 返回后，用 Bash 工具更新 state.json `phase` 为 `"tailored"`。

**【全流程】⚠️ 不得回显或复述 tailor 的返回内容，state.json 更新完毕后立即执行 Step 7，不得有任何文字输出。**

---

## Step 7：生成 shortlist.html

（全流程最后一步，**只产 HTML，不产 MD 文件**）

**执行顺序（严格按此顺序）：**

1. 用 Bash 工具：探测 Python，调用 `build_html.py` 生成 shortlist.html
2. 用 Bash 工具：更新 `state.json` 的 `phase` 为 `"done"`
3. **最后一步**：在聊天里输出一条简短的完成消息（**只含 HTML 链接 + 操作提示**，不再输出排名 MD 概览，因为 HTML 视图就是用来看这些内容的）

执行命令：

```bash
# 1. 探测可用的 Python 命令
if command -v python3 >/dev/null 2>&1; then
  PYBIN=python3
elif command -v python >/dev/null 2>&1; then
  # 兜底（Windows 上常见只有 python 没有 python3）
  PYBIN=python
else
  PYBIN=""
fi

# 2. 调用脚本，或在没有 Python 时输出降级标记
if [ -n "$PYBIN" ]; then
  "$PYBIN" "$HOME/.claude/skills/job-hunt/build_html.py" "<data_dir>" "<run_id>"
else
  echo "PYTHON_MISSING"
fi
```

**Bash 输出解析规则**（决定最终向用户发的完成消息）：

| 脚本 stdout 内容 | 状态 |
|---|---|
| `OK: ...`（无论后续 `OPENED` 或 `OPEN_FAILED`） | HTML 生成成功，提示用户复制链接到浏览器打开 |
| `SKIP: template not found` | 模板文件缺失（极少发生，提示重装） |
| `PYTHON_MISSING` | 系统没装 Python 3，引导用户安装（流程失败，因为现在只产 HTML） |
| `ERROR: ...`（stderr） | 其他错误，告知用户排查 |

**脚本设计原则**：
- LLM 完全不构造 JSON，由脚本自己扫描 jd-pool / tailored 目录读取所有字段
- 空文件优雅处理：tailored 文件不存在 → 该字段为空字符串
- 模板缺失优雅降级：template.html 不存在 → 打印 `SKIP:` 退出 0，主流程不中断

更新 state.json `phase` 为 `"done"` 之后，**必须**在聊天里输出最终消息——根据 Bash 输出的标记选对应文案。

**最终消息文案（4 种分支）：**

- **HTML 生成成功**（Python 脚本输出 `OK: ...`）：
  ```
  ✅ 全部完成！共生成 <N> 份定制简历。

  🌐 HTML 视图：file://<data_dir 绝对路径>/output/<run_id>/shortlist.html

  💡 复制上面的 file:// 链接到浏览器地址栏打开，即可查看完整简历视图。页面支持：
     · 按匹配度排序查看所有岗位
     · 在网页上直接编辑简历（自动保存到浏览器本地）
     · 一键导出为 PDF（中文字体跨平台一致）
     · 切换查看每个岗位的简历 / 改动 / 开场白
  ```

  其中 `<N>` 用 `state.json.stages.tailored` 数组长度填充。

- **HTML 模板缺失**（Python 脚本输出 `SKIP: ...`）：
  ```
  ⚠️ 流程已完成，但 HTML 视图未生成。

  原因：未找到 ~/.claude/skills/job-hunt/template.html。
  解决：请重新安装 skill（运行 install.sh 或 npx skills add）。

  各岗位定制简历已写入：<data_dir 绝对路径>/output/<run_id>/tailored/
  ```

- **Python 不可用**（Bash 输出 `PYTHON_MISSING`）：
  ```
  ⚠️ 流程已完成，但 HTML 视图未生成。

  原因：你的系统没有 Python 3。

  📦 装上 Python 3 之后即可补生成 HTML 视图：

     • Mac：终端跑 `xcode-select --install`（一次性，几分钟）
     • Windows：访问 https://python.org/downloads/ 下载安装
        ⚠️ 安装时务必勾选「Add Python to PATH」
     • Linux：通常已自带；若没有 `sudo apt install python3` 或 `sudo yum install python3`

  装完之后，跑这条命令补生成 HTML：

     python3 ~/.claude/skills/job-hunt/build_html.py "<data_dir 绝对路径>" "<run_id>"

  各岗位定制简历已写入：<data_dir 绝对路径>/output/<run_id>/tailored/
  ```

- **其他错误**（脚本 stderr 输出 `ERROR: ...`）：
  ```
  ⚠️ HTML 生成失败。错误信息：<stderr 内容>

  各岗位定制简历已写入：<data_dir 绝对路径>/output/<run_id>/tailored/
  请检查文件或重新运行。
  ```

⚠️ **关键约束**：
- file:// 链接**不要用反引号包**——纯文本链接更容易让用户三击全选复制到浏览器
- 完成消息必须是整条回复的**最后一段**，不得放在中间
- **不再生成 shortlist.md**，也**不在聊天里输出 MD 内容概览**——HTML 视图就是给用户看的，重复输出冗余且占屏

---

## Step 7b（status 子命令）：输出运行状态

扫描 `<data_dir>/output/` 下所有子目录，找名字格式为 `YYYY-MM-DD-HHMM` 的目录，读取最新一个的 `state.json`，按以下格式输出：

```
Job-Hunt 状态报告
==================
Run ID：<run_id>
当前阶段：<phase>
工作目录：<data_dir>

进度统计：
  已导入 JD：<stages.fetched 数量> 个
  完成分析：<stages.analyzed 数量> 个
  分析失败：<stages.analysis_errors 数量> 个
  生成三件套：<stages.tailored 数量> 个

最后更新：<checkpoint_at>
```

若无任何 run 记录，告知用户「尚未运行过 /job-hunt，请先运行完整流程。」

---

## Step 8（clean 子命令）：强制清理

删除 `<data_dir>/.work/jd-pool/` 下所有文件（含 .analysis.md）。
删除 `<data_dir>/output/` 下所有 run 目录。
删除 `<data_dir>/.work/resume.md`（若存在）。
统计并告知用户清理了多少文件/目录。

