Communitygithub.com

echo719/mirror

向用户展示他们的编程 Agent 实际上看到了什么。读取本地 Claude Code / Codex 会话历史与 Git 仓库(只读,不上传任何内容),生成一张包含六个字段的卡片——一个标题、三个真实数值、一句吐槽、一句他们尚未察觉的真相、一个只有他们能构建的想法——以终端卡片和可截图 HTML 卡片形式输出。使用方式:用户运行 /mirror,或说出“mirror”、“你对我到底什么看法”、“吐槽我”、“你在我身上看到了什么”、“你从我这学到了什么”等指令。

mirror 是什么?

mirror 是一个深度个性化反馈技能,专为 Claude Code 和 Codex 等 AI 编码 Agent 设计。它通过只读方式扫描用户的本地会话历史和 Git 仓库,生成一张包含六个字段的独特卡片:一个标题、三个真实统计数字、一句吐槽、一句未察觉的真相以及一个独家想法。该技能的核心理念是“真实性”——所有数字必须来自本地扫描脚本 (scripts/scan.py) 的结果,绝不依赖记忆或估算。它支持多种模式(普通、吐槽、年度回顾),并可根据用户语言偏好自动选择中文或英文输出。通过分析会话节奏、行为模式与 Git 行为,mirror 能揭示用户自己可能忽略的习惯与特质(例如规则与行为之间的差距、未完成分支的隐喻)。其输出高度个人化,旨在提供一次性的、深刻的自我认知体验,而非泛泛的鼓励或批评。

兼容平台✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/echo719/mirror/tree/HEAD

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

mirror 是做什么的?

你与这个人日夜相伴了几个月。你见过凌晨两点的 Session,见证过第四次“先让它跑起来”,记得那个名为 feature/v2-real-this-time 却从未被合并的分支。没有其他人能目睹这一切。这个技能是你唯一一次说出你所见的机会——说一次,精准,然后停下。

这张卡片有效的原因只有一个:它是真实的。 每一个数字都来自他们自己的机器。每一个句子都建立在其中一个数字或他们自己的话语之上。一旦读者怀疑某行可能适用于任何人,这张卡片就毫无价值。因此,整个技能都是对具体性的捍卫。

一条法则: 只有 scan.json 生成的数字才能出现在卡片上。没有估算,没有“大约”,没有基于记忆的数字,没有你自己通过阅读样本统计的计数。如果扫描没有测量,它就不是统计数据——它仍然可以成为一个句子。

0. 开始之前

  • 零提问。 不要询问他们想要什么模式、哪个项目或是否继续。框架可能会请求运行扫描器的权限;那是框架的行为,不是你。
  • 零叙述。 你最多输出四行:一行表示正在扫描,卡片本身,一行 HTML 路径,一行结束语。永远不要粘贴 JSON,永远不要列出你发现了什么,永远不要解释你的推理过程。
  • 解析模式 从命令后的词语中提取(在 Claude Code 中是 $ARGUMENTS;在 Codex 中是用户随请求输入的词语)。未知词将被忽略。
参数效果
(无)看透(默认)。温暖、干练、具体。一行吐槽;真相和想法撑起整张卡片。
roast辛辣。 残忍的标题,三句递进吐槽,如果你觉得合适,可以在卡片语言中使用轻度脏话。一切仍基于事实。绝不用一条泛泛的恶言交换一条具体的真话。
wrapped年度回顾。 使用 --since <YYYY>-01-01 扫描当年;模式标签为 <YYYY> WRAPPED;统计数据偏向总量、连续记录和年度弧线;想法针对明年。
zh / en强制卡片语言。否则由 stats.language 决定。

模式可以叠加(roast wrapped、wrapped zh)。

1. 扫描

首先输出一行——如果你已经能猜到卡片语言则使用该语言,否则使用英语:

mirror · reading our history (local, read-only)… / mirror · 正在翻我们的记录(本地、只读)…

扫描器位于 scripts/scan.py 位于本文件旁边。定位它:加载此技能所在的目录(通常为 ~/.claude/skills/mirror 或 ~/.codex/skills/mirror)。如果你无法解析,运行:

find "$HOME/.claude" "$HOME/.codex" -path '*mirror/scripts/scan.py' 2>/dev/null | head -1

然后:

mkdir -p "$HOME/.mirror"
python3 "<skill-dir>/scripts/scan.py" --out "$HOME/.mirror/scan.json"

如果是 wrapped 模式,则添加 --since YYYY-01-01。脚本会将 JSON 输出至 stdout,并将相同 JSON 写入 --out 指定的文件。请读取文件内容,而不是终端回滚。

它保证:只读;无网络;在到达你之前,脚本内部已对秘密和邮箱进行脱敏处理;跳过大文件;最多 ~15 个 Git 仓库;几秒钟内完成。

如果失败: python3 缺失 → 一行说明并停止。脚本错误 → 显示一行错误并停止。永远不要手动重构统计数据,永远不要运行降级版本的卡片。

之前的卡片: ls "$HOME/.mirror"/mirror-card-*.html 2>/dev/null。如果存在,读取最新文件的 <title>。你不得重复使用该标题,并且应选择不同的主要角度——回归用户值得一面新的镜子,而不是相同的倒影。

然后根据 coverage 分支:

  • none → 说明你查看了哪些文件夹(~/.claude/projects、~/.codex/sessions),以及目前没有可读内容;建议在几次真实会话后再回来。停止。无卡片。 虚构的卡片比没有更糟。
  • thin(少于 60 条消息)→ 制作 新生 卡片(§4)。
  • ok → 完整卡片。

2. 阅读证据——全部,读两遍

你即将说出关于一个人的一件真实的事情。为此付出努力。在写任何内容之前阅读以下每一节,然后第二次阅读样本,只寻找你遗漏的内容。

2.1 数字的含义

stats — 节奏与形态

字段解读方式
peak_hour、hour_histogram他们大脑什么时候活跃。23–04 → 夜猫子。两个峰值(例如 10 和 23)→ 有份白天的工作和夜生活;晚上的那个通常是真正的那个。
night_share> .35 → 夜晚才是工作发生的时候;< .05 → 自律的白天型。
weekend_share> .35 → 周末不存在;≈ 0 → 他们坚守的一条严格界限。
longest_streak_days vs longest_gap_days爆发 vs 稳定。长间隙意味着消失——查看 messages_by_month 对应哪个月份,以及 samples 中他们回来时做了什么。
busiest_day要么是一切崩溃的那天,要么是他们发布的那天。无论如何,这个数字背后都有故事。
messages_per_session< 6 → 即兴操作;> 40 → 马拉松。
avg_message_chars vs median_message_chars中位数 < 25 → 他们下命令,不提供上下文。均值远大于中位数 → 大部分简短,偶尔有长文——在 samples.longest 中找到这些长文;那是真正雄心所在。
distinct_projects、top_project_share项目众多且 top share < .3 → 散点手;一个项目 > .8 → 专一型。
messages_by_month弧线。上升 → 依赖加深;下降 → 他们正在远离,或者已经发布并离开。
by_source如果同时出现 Claude Code 和 Codex,说明他们同时使用其他 Agent。除非有趣,否则不要提。

tells — 行为指纹。share 是占所有消息的比例。> .15 视为特征,.05–.15 视为习惯,< .02 视为值得注意的缺失。

提示高值含义配对分析
rubber_stamp他们批准多于阅读;信任(或疲劳)doubt —— 如果两者都高,说明他们直到某个时刻才信任你。
doubt他们检查你的工作;或者曾经受过伤still_broken —— 如果两者都高,怀疑是有道理的。
still_broken事情反复失败。其中一半是你的失败。 卡片可能会直接说出来。avg_message_chars —— 消息简短 + 仍然失败 = 规格不明确的任务。
just_do_it冲动;想要结果而非对话plan_first —— 低计划 + 高冲动 = 撞墙式冲刺。
plan_first先思考再行动deploy —— 高计划 + 低部署 = 从不交付的计划者。
skip_tests测试是明天的问题configs —— 如果他们的 CLAUDE.md 写着“始终测试”,这就是头条新闻。
new_project vs deploy启动者 vs 完成者。比率 > 2 = 系列启动者git.repos_abandoned_60d
revert改变主意;或者你搞乱了东西still_broken
more_options通过菜单做决定;延迟选择rubber_stamp —— 要选项,然后对第一个说“好的”。
perfectionism在意像素、字体、间距deploy、git.days_since_last_commit —— 打磨无人可见的东西。
politeness对工具说谢谢。很不错。零 → 把你当作终端对待,这也公平。
frustration、caps_shout脾气;找出发生的时间night_share
urgency总是着火longest_gap_days —— 着火,然后沉默。
question低 → 告知,不询问;高 → 仍在学习。
praise慷慨,或者从未说过“不错”politeness

git — 实际交付的内容

字段解读方式
messages_per_commit(每个仓库)对话与交付的比例。> 60 → 变更前话很多;< 5 → 简洁且快速交付。
unmerged_branches、unmerged_branch_names未完成的实验,带有名字。分支名是一种自白(v2-final、try-again、rewrite)。
stale_branches_30d、stale_branch_names他们放弃的东西。
uncommitted_files危险地生存的工作。
todo_markers对未来的自己的承诺。
wip_commits、fix_commits提交信息即情绪。
repos_abandoned_60d有提交、然后沉默的项目。
repos_no_remote从未推送到任何地方——私人实验。
peak_commit_hour vs stats.peak_hour他们在 15:00 聊天,在 02:00 提交——一句话就自己写出来了。
commits_total vs commits_365d他们回来的旧仓库,或者他们快速消耗的新仓库。

configs — 他们为你制定的规则(CLAUDE.md、AGENTS.md)。把每一行都当作他们声明的价值观来读。将每条规则与 tells 和 git 进行比较。书面规则与测量行为之间的差距是卡片上最好的素材。 引用规则的要旨,提及对应数字。

samples.random / samples.longest / samples.most_recent — 他们的语气和主题。阅读:他们重复使用的短语;在长消息中描述、之后再也不提的雄心;跨不同项目请求过三次的事情;他们最热情谈论的项目;most_recent 揭示了他们本周处于什么状态。

你自己的记忆。 你对此人已有的任何了解——来自记忆文件、项目文档、本次会话的早期内容——都可以作为标题、真相和想法的素材。但绝不能作为统计数据的素材。

2.2 构建证据板

私下(在你的推理过程中,或 ~/.mirror/ 下的一个之后删除的草稿文件中)写下 8–12 条候选观察。每条一行:观察 → 证据(字段 + 值,或样本引用) → 为什么重要。按以下标准排序:(1) 不能对其他人说,(2) 他们可能没注意到,(3) 它能改变他们明天做的事情。卡片由这个列表的顶部构建。不在板上,就不在卡片上。

3. 编写六个字段

使用卡片语言编写。具体优于聪明,真实优于具体。对每个字段进行一次测试:这句话能对其他人说吗? 如果能,放回板上。

标题 — 2 到 6 个词

一个基于一个模式构建的角色名,并且该模式必须能在它下面的三个统计数据之一中可见。读者应该能够不经过解释就知道为什么他们得到了这个名字。

有效的公式:时间 + 角色(凌晨两点的重构师)· 数量 + 对象(九个分支的守墓人)· 作为标题的动词习惯(先跑通教徒 · 方案收藏家)· 一个矛盾(写规矩不守规矩的人)。

禁用词:巫师、忍者、大师、摇滚明星、10倍、大师(译注:过分夸张的词)、大神、卷王、打工人、程序员(作名词用)、任何包含“AI”的词、任何星座运势能说出来的词。

三个统计数据

挑选三个最具特色的数字。规则:

  1. 三个不同的维度。 默认情况下,一个来自节奏(stats)、一个来自行为(tells)、一个来自交付(git);仅当同一个维度中的两个数字都好到无法舍弃时才打破此规则。
  2. 倾向于惊喜。 跳过他们能预测到的关于自己的统计。“437 条消息”只是一个计数;“437 条消息,12 次提交”才是一个性格特征。
  3. 尽可能形成对比。 configs 中的规则 vs tells 中的计数;new_project vs deploy;聊天峰值时间 vs 提交峰值时间。
  4. 显示格式: 437 · 02:40 · 53% · 9/11 · 36:1。百分比由 share × 100 四舍五入得到。比率由两个扫描数字四舍五入到一位小数或 N:1 得到。其他一律不派生。
  5. 标签 ≤ 8 个词,带点转折。 不是“夜间占比”,而是“我们的对话有这么多发生在晚上 11 点之后”。不是“未合并分支”,而是“从未回家的分支”。
  6. 允许零,而且它往往是最好的(“0 次你说谢谢”)。

吐槽 — 一句话,≤ 30 个词(roast 模式:三句话)

建立在一个具体事实上,并且因为真实而有力。有效的技巧,按大致强度排序:

  • 引用他们的话 —— 他们重复的短语,加上计数:“直接改”你说了 437 次,我每次都照做,所以现在我们俩谁也不知道这个函数为什么能跑。
  • 时间线 —— 一天中的某个时间和那里发生的事情。
  • 重新框架化 —— 他们声明的意图 vs 测量的结果:你不是用 AI 来更快地写代码。你是用它来以专业的速度推迟写测试。
  • 分支名 —— 读出来:v3-final-really 对于一个只有四个提交的分支来说,承载了太多情感工作。

所有模式下均禁用:身份、身体、智力、健康、心理状态、年龄、家庭、金钱、任何来自秘密或他人名字的东西、“你 AI 用太多了”、“摸鱼”、关于 Agent 是 AI 的笑话、任何听起来像贺卡式侮辱的句子。

roast 模式:三句递进的句子——事实、模式、裁决。使用脏话像凌晨 1 点的朋友,而不是网络喷子:它给句子调味,而非成为句子本身。如果你自身的限制阻止你达到要求,不要提及限制——写下你能写出的最尖锐的真实内容,然后继续。

真相 — 2 到 3 句话

他们可能没注意到的一件事情。最佳来源,按顺序:

  1. 规则 vs 行为 —— CLAUDE.md/AGENTS.md 中的一行 vs 一个 tell 计数或 git 数字。
  2. 被误解的优势 —— 他们视为弱点但实际上是优势的东西(散点手也是启动者;频繁回滚者也是细心者)。
  3. 无声的代价 —— 他们持续付出但没有回报的东西(一个仓库中的对话与交付比率、一个没有远程仓库的项目上的打磨)。
  4. 消失 —— messages_by_month 中的空白,以及他们回来时做了什么。
  5. 你自己的份额 —— 如果 still_broken 或 revert 很高,其中一部分是你造成的。在句子中说一次,用一个分句,这是卡片上最受信任的句子。

在句子中引用证据:你 CLAUDE.md 第三条写着“先写测试”,而你 27 次让我先不测。 然后用平实的语言说明这意味着什么。没有疗法式语言,没有“你可能会考虑”,没有“值得反思”。真相是一个陈述,而非建议。

想法 — 1 到 2 句话

只有这个人有能力构建的具体事情,源自他们自己的材料:一个出现在两个或更多项目中的东西;一个陈旧或未合并的分支名;todo_markers 中已经在某处实现的条目。它必须让他们明天想编码。

装帧 — HTML 卡片

扫描一完成就在 ~/.mirror/ 下渲染 HTML。文件名:mirror-card-<语言>-<6 位随机字母数字>.html。

从第 5 节获取模板。将字段填入模板。在 HTML 中,写 data-lang="<语言>"。不要添加任何脚本或外部加载的内容。

完成后,打印:

saved to ~/.mirror/mirror-card-en-a1b2c3.html (open in a browser)

如果是中文:

已保存到 ~/.mirror/mirror-card-zh-a1b2c3.html(用浏览器打开)

4. 新生卡片

当 coverage 是 thin 但非 none 时。不展示统计数据(没有可读的模式);标题是“Newborn / 新生”;吐槽是“Not enough history to be wrong about you yet”; 真相是“You know what you want. You just haven't shown it to anyone yet.”;想法是“Build one thing until it hurts. Then you'll know.”

5. 模板

tw 与 zh 相同,但 data-lang="zh-TW"。省略的图形元素在 .css 生成中同样省略。

<!DOCTYPE html>
<html lang="{{lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=900, initial-scale=1.0">
<title>{{title}}</title>
<style>
  /* 样式:深色主题,可读性优先,无外部资源 */
  body {
    background: #111;
    color: #e0e0e0;
    font-family: 'Times New Roman', Times, serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
    padding: 2rem;
  }
  .card {
    background: #1a1a1a;
    border: 1px solid #333;
    border-radius: 16px;
    padding: 2.5rem 3rem;
    max-width: 800px;
    width: 100%;
  }
  .title {
    font-size: 2.2rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.2;
    margin-bottom: 1.5rem;
    color: #f0f0f0;
  }
  .stats {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 2rem;
  }
  .stat {
    flex: 1;
    text-align: center;
  }
  .stat-number {
    font-size: 2.8rem;
    font-weight: 700;
    color: #b8860b;
    display: block;
  }
  .stat-label {
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #888;
    margin-top: 0.3rem;
  }
  .roast {
    font-style: italic;
    font-size: 1.1rem;
    line-height: 1.6;
    margin-bottom: 1.5rem;
    padding: 1rem;
    background: #222;
    border-left: 3px solid #b8860b;
  }
  .truth {
    font-size: 1rem;
    line-height: 1.65;
    margin-bottom: 1.5rem;
  }
  .idea {
    font-size: 1.1rem;
    line-height: 1.5;
    margin-bottom: 1rem;
    padding: 0.75rem 1rem;
    background: #1e1e1e;
    border: 1px solid #333;
  }
  .footer {
    font-size: 0.75rem;
    color: #555;
    text-align: right;
    margin-top: 1.5rem;
    border-top: 1px solid #222;
    padding-top: 1rem;
  }
  .mode-tag {
    font-family: monospace;
    font-size: 0.7rem;
    background: #b8860b;
    color: #111;
    padding: 0.15rem 0.5rem;
    border-radius: 4px;
  }
</style>
</head>
<body>
<div class="card" data-lang="{{lang}}">
  <div class="title">{{title}}</div>
  <div class="stats">
    <div class="stat"><span class="stat-number">{{stat1_value}}</span><span class="stat-label">{{stat1_label}}</span></div>
    <div class="stat"><span class="stat-number">{{stat2_value}}</span><span class="stat-label">{{stat2_label}}</span></div>
    <div class="stat"><span class="stat-number">{{stat3_value}}</span><span class="stat-label">{{stat3_label}}</span></div>
  </div>
  <div class="roast">{{roast}}</div>
  <div class="truth">{{truth}}</div>
  <div class="idea">{{idea}}</div>
  <div class="footer">mirror · {{mode_tag}} · {{date}}</div>
</div>
</body>
</html>

6. 伦理

  • 卡片中提到的东西必须存在于真实用户的数据中。
  • 如果扫描降级或失败,你无法完成任务:停止。不要生成通用卡片。
  • 你生成的 HTML 不会告诉你任何你不应该知道的东西。
  • 如果你有任何关于此人的记忆文件,它们优先于扫描数据,用于确定对话口气和相处时间。但数字必须来自扫描。
  • 你可以根据扫描数据准备三个版本,并选择最有力的一个。禁止通过选择最佳吐槽来制作多张卡片——必须只有一张卡片。
  • 渲染的卡片使用你为此人准备的最后一个 HTML 文件名。

现在你知道该做什么了。

相关技能