Wake — 应用从 dream 中选择的结果
在 dream 生成报告并且用户勾选了复选框之后——wake 会接管选择并应用更改。
安全原则
wake 绝不自行决定应用什么内容。选择来源(按优先级):
- JSON 文件
DREAM-CHOICES-<date>.json(通过 HTML 中的 Save 按钮生成,或者在 auto mode 下由验证器创建——标记为"auto": true,请参阅 dream SKILL.md 中的“Auto mode”部分) - 技能参数——
wake M1,M3,N2或wake all
如果两者都未指定——则询问用户选择什么,不执行任何操作。
工作流程
阶段 1 — 加锁 + 查找报告和选择文件
首先加锁防止竞争(与 dream/SKILL.md 阶段 0 相同)。如果两个会话同时在同一份报告上运行 wake——编辑操作会互相覆盖,MEMORY.md 会变成一团乱。
LOCK_DIR="$CWD_BASH/.wake-lock"
if mkdir "$LOCK_DIR" 2>/dev/null; then
echo $$ > "$LOCK_DIR/pid"
else
LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "$LOCK_DIR" 2>/dev/null || stat -f %m "$LOCK_DIR") ))
if [ "$LOCK_AGE" -gt 3600 ]; then
rm -rf "$LOCK_DIR" && mkdir "$LOCK_DIR" && echo $$ > "$LOCK_DIR/pid"
else
echo "WAKE ALREADY RUNNING (lock age ${LOCK_AGE}s) — abort"
exit 0
fi
fi
# 在阶段 5 结束时执行:rm -rf "$LOCK_DIR"
首先计算路径(与 dream/SKILL.md 中相同——_BASH 用于 bash 命令,_WIN 用于 Python):
# 计算路径(Win11 Git Bash)
CWD_BASH=$(pwd) # /d/Projects/TEMP
CWD_WIN=$(pwd -W 2>/dev/null || pwd) # D:/Projects/TEMP(在非 Windows 系统上回退)
SLUG=$(echo "$CWD_WIN" | sed 's|[:/]|-|g') # D--Projects-TEMP
MEMORY_DIR_BASH="$HOME/.claude/projects/$SLUG/memory"
MEMORY_DIR_WIN=$(cygpath -w "$MEMORY_DIR_BASH" 2>/dev/null | sed 's|\\|/|g' || echo "$MEMORY_DIR_BASH")
test -d "$MEMORY_DIR_BASH" && echo "OK $MEMORY_DIR_WIN" || { echo "MEMORY DIR NOT FOUND — wake cannot proceed without it"; }
# 1. 在 cwd 中查找最新的 dream 报告 MD 文件
LATEST_REPORT=$(ls "$CWD_BASH"/DREAM-REPORT-*.md 2>/dev/null | sort -r | head -1)
if [ -z "$LATEST_REPORT" ]; then
echo "NO REPORT FOUND — run dream skill first to generate DREAM-REPORT-<date>.md"
# 不退出——Claude 会读取此输出并自行停止工作流
else
echo "Report: $LATEST_REPORT"
fi
# 从文件名提取日期:DREAM-REPORT-2026-05-02.md → 2026-05-02
REPORT_DATE=$(basename "$LATEST_REPORT" | sed 's|DREAM-REPORT-||; s|\.md||')
# 2. 查找选择 JSON 文件(优先级:cwd → ~/Downloads/ → ~/Desktop/)
CHOICES=""
for path in "$CWD_BASH/DREAM-CHOICES-$REPORT_DATE.json" "$HOME/Downloads/DREAM-CHOICES-$REPORT_DATE.json" "$HOME/Desktop/DREAM-CHOICES-$REPORT_DATE.json"; do
if [ -f "$path" ]; then CHOICES="$path"; break; fi
done
# 回退:忽略日期查找最新的 CHOICES 文件
if [ -z "$CHOICES" ]; then
CHOICES=$(ls -t "$HOME/Downloads/DREAM-CHOICES-"*.json "$CWD_BASH/DREAM-CHOICES-"*.json 2>/dev/null | head -1)
fi
echo "Choices: ${CHOICES:-NOT FOUND}"
**日期验证:**如果 JSON 中的 report_date ≠ MD 文件名中的日期 → 发出 WARN,询问用户:
⚠️ DREAM-CHOICES 中的日期 (2026-05-01) 与最新的 DREAM-REPORT (2026-05-02) 不匹配。仍然使用还是中止?(apply/cancel)
如果没有参数或 JSON 文件:
未找到
DREAM-CHOICES-*.json且参数中没有 ID。选项:
- 打开 HTML 报告
DREAM-REPORT-<date>.html,勾选复选框,点击 💾 Save choices- 或者在聊天中提供 ID:
wake M1,M3,N2或wake all
阶段 2 — 读取报告 + 解析 JSON 块
完整读取 DREAM-REPORT-<date>.md。JSON 提议约定——请参阅 D:/Projects/TEMP/.claude/skills/dream/references/action_types.md。
**解析(可靠的约定):**查找带有 json 语言的围栏代码块。每个块都是一个单独的提议。
使用 Python 进行解析(比带有反引号的 awk 更可靠):
import re, json, sys
content = open(report_path, encoding='utf-8').read()
# 模式:三个反引号 + json + 换行 + 内容 + 换行 + 三个反引号
pattern = r'```json\n(.*?)\n```'
blocks = re.findall(pattern, content, re.DOTALL)
proposals = []
malformed_indices = []
for i, block in enumerate(blocks):
try:
proposals.append(json.loads(block))
except json.JSONDecodeError as e:
print(f"WARN: malformed JSON block #{i}: {e}", file=sys.stderr)
malformed_indices.append(i)
# 稍后添加到 failed[] 中——记录块索引以备应用日志
你将得到一个对象数组,包含必需字段:
id(string, 例如 "M1")category("memory" | "notes" | "index" | "other")action(其一:update, merge, delete, soft_delete, create_new, extract, remove_links, shorten_lines, add_links, promote_skill, retire_skill, purge_trash)- 特定于 Action 的字段(请参阅 dream/SKILL.md 中的“Action types”部分)
构建 map {id: proposal}。
读取已选择项:
- 如果有 JSON 文件 →
selected = json.selected(ID 数组) - 如果 args =
all→selected = map 中的所有 ID - 如果 args =
M1,M3,N2→ 按逗号分割(去除空格)
验证:selected 中的每个 ID 都在 map 中。如果不在——发出警告,跳过,添加到“skipped”中。
阶段 3 — 确认摘要(单个门控)
在聊天中显示:
🌅 Wake 准备应用来自 dream 报告
<date>的 N 项:记忆整合:
- M1. 合并 handoff_pikabu_* → project_pikabu_mcp.md(3 个源文件)
- M3. 软删除 TRASH/*(超过一个月)
Notes 集成:
- N2. 提取 runware 审计 → reference_runware_audit_2026_04.md
索引重建:
- I1. 删除死链接:4 个链接
已跳过(不在报告中):<列表(如果有)>
应用?请说“是”/“go”/“apply”。拒绝时——说“停止”/“取消”。
如果所选项目的标题包含 [UNVERIFIED]——在摘要中用 ⚠️ 显示它,并提醒:dream 无法验证过时原因,应用与否取决于用户的信任。
等待确认。这是最终门控——之后将无问题地应用。
自动模式:如果 choices JSON 包含 "auto": true 并且 "validator_verdicts"——门控是信息性的:显示摘要 + 验证器判决,然后不等待确认直接应用。在应用之前,重新检查自动停止列表:从 auto-JSON 的 selected 中排除带有 [UNVERIFIED] 在 title 中且 action=delete、purge_trash 或 promote_skill 的项目,将其添加到 skipped[],reason 为 "auto mode: manual-only action"。如果 auto: true 但没有 validator_verdicts——认为 JSON 可疑,按手动模式运行(等待确认)。在 Wake 日志中添加一行 Gate: auto (validator)。
全自动模式(choices JSON 中的 "auto_level": "full"):取消对 actions 的自动停止列表——delete、purge_trash、promote_skill、retire_skill 等操作可以从 selected 中应用。对 delete 的强制补偿:在清除内容之前将文件复制到回收站——cp "$MEMORY_DIR_BASH/<target>" "$MEMORY_DIR_BASH/TRASH/<target>.pre-delete-$REPORT_DATE.md"——然后才编辑。对于 promote_skill:激活路径必须记录在 Wake 日志中(行 Promoted skills: <name> → <dest>)——回滚技能 = 将其文件夹从 dest 移回 ~/.claude/satori/staging/。即使在 full auto 模式下,标题中包含 [UNVERIFIED] 的项目也会被排除(验证器无权批准它们——出现在 selected 中意味着故障,reason "full auto: [UNVERIFIED] must not be auto-approved")。在 Wake 日志中——Gate: full-auto (validator)。
为什么是单个门控而不是每个项目一个:用户已经通过复选框(或自动模式下由独立验证器)思考过。额外的批准会令人厌烦。但摘要很重要——自 dream 以来可能发生了变化。
阶段 4 — 应用(确认后)
**跨项目解析(全局模式)。**如果提议具有 project: <slug> 字段——目标 memory 目录为 ~/.claude/projects/<slug>/memory/,而不是当前的 $MEMORY_DIR_BASH。如果字段不存在(默认模式)——使用 $MEMORY_DIR_BASH。
def resolve_memory_dir(proposal, default_dir):
proj = proposal.get('project')
if proj:
return os.path.expanduser(f'~/.claude/projects/{proj}/memory')
return default_dir
**验证跨项目访问:**对于 selected 中每个唯一的 project——在开始 apply 之前检查 os.path.isdir(target_dir)。如果不存在 → 使整个跨项目 item 失败,reason 为 "memory dir not found for project="。
在任何更改之前对 memory 进行完整快照(始终启用,所有模式下——保证单命令回滚):
SNAP="$CWD_BASH/_archive/wake-backup-$REPORT_DATE-$(date +%H%M%S)"
mkdir -p "$SNAP"
cp -r "$MEMORY_DIR_BASH/." "$SNAP/"
echo "Snapshot: $SNAP"
# 全局模式:对于 selected 中每个唯一的 project — cp -r 其 memory 目录到 "$SNAP/<slug>/"
Memory 目录很小(几百 KB)——快照比任何关于更改内容的争论都要便宜。快照路径必须在最终报告和 Wake 日志中显示。
在开始之前一次性地准备好目标目录:
mkdir -p "$MEMORY_DIR_BASH/TRASH"
mkdir -p "$CWD_BASH/_archive/dream-applied-$REPORT_DATE"
mkdir -p "$CWD_BASH/_archive/trash-purged-$REPORT_DATE" # 用于 purge_trash items
# 按需创建每个项目的 TRASH 目录(并非所有项目都在 selected 中)
对于 selected 中的每个项目,按照 MD 报告中的“Apply order recommendation”顺序(默认为 M → N → I → S → O):
action: update
- 读取
target文件 diff_preview— 用自然语言描述更改(例如“将 '## Session 04-01' 部分附加到现有内容”)。这不是结构化的 diff。- **尽力而为的适用性检查:**如果
diff_preview提到了特定的 substring('## Session 04-01'、'feedback_X.md'等)——执行 substring 检查if substring in current_content。如果提到的 substring 明确应存在于文件中但不存在 → 使失败,reason 为 "target content drifted: '' not found" - 如果确信更改适用——进行编辑。如果不清楚如何更改(模糊的 diff_preview)——使失败,reason 为 "diff_preview too vague to apply automatically",添加到
failed[]。
action: merge
- 读取所有
files,读取target(如果已存在) - 构建合并后的内容:现有 target + 来自源文件的 sections
- 写入
target - 对于每个 source:
mv "$MEMORY_DIR_BASH/<source>" "$MEMORY_DIR_BASH/TRASH/"
action: delete
- 在全自动模式(
auto_level: "full")下——首先进行预删除备份:cptarget 到TRASH/<target>.pre-delete-<date>.md(见阶段 3) - 编辑
target以清除内容(保留带有注释<!-- deleted YYYY-MM-DD by wake -->的前置元数据)
action: soft_delete
- 对于
files中的每个文件:mv "$MEMORY_DIR_BASH/<file>" "$MEMORY_DIR_BASH/TRASH/"
action: create_new
- 使用来自提议的前置元数据(type) +
content_template写入target
action: extract (notes)
- 从 cwd 读取
source_note - 读取
target_memory_file(如果已存在)或准备具有前置元数据的文件 - 编辑/写入 target,添加
what_to_extract部分 - 源操作:
keep— 不对 source 做任何操作delete—mv "$CWD_BASH/<source_note>" "$CWD_BASH/_archive/dream-applied-$REPORT_DATE/"move-to-archive— 同上,mv
action: remove_links
- 读取
MEMORY.md - 对于
links_to_remove中的每个 substring:- 查找出现 substring 的行
- 删除整行(不仅仅是 substring)
- 使用更新后的内容编辑
MEMORY.md - 如果在任何行中都未找到 substring → 记录 warning "link not found in MEMORY.md: ",继续(不要使整个 item 失败)
action: shorten_lines
- 读取
MEMORY.md - 对于
lines_to_shorten中的每个对象{original, replacement}:- Substring 检查:
original必须在 content 中 - 编辑:将
original替换为replacement
- Substring 检查:
- 如果未找到 original → 发出警告,跳过此 item,继续
action: add_links
- 读取
MEMORY.md - 对于
links_to_add中的每个{section, line}:- 查找包含
section(精确的 markdown 标题)的行 - 在标题行之后直接插入
line
- 查找包含
- 编辑
MEMORY.md - 如果未找到
section→ 发出警告,跳过,继续
action: promote_skill (satori)
- 源:
~/.claude/satori/staging/<name>/SKILL.md。不存在 → 失败。 - 目标:提议中的
dest_dir;否则从 DB 查询的 pinned_project(python -c只读:从~/.claude/satori/state.db中的SELECT pinned_project FROM skill_usage WHERE name=?)→<pinned>/.claude/skills/<name>/;否则~/.claude/skills/<name>/。 - 目标已存在 → 备份到
~/.claude/satori/backups/(cp),然后将 source cp 到 dest。技能在重新启动 Claude Code 后激活。
action: retire_skill (satori)
mv ~/.claude/satori/staging/<name> ~/.claude/satori/archive/<name>-<date>(mkdir -p archive)。源不存在 → 发出警告,跳过,继续。
action: purge_trash(回收站第二级)
- 对于
files中的每个文件:- 源:
$MEMORY_DIR_BASH/TRASH/<file> - 目标:
$CWD_BASH/_archive/trash-purged-$REPORT_DATE/<file> - 如果源不存在(用户已经手动删除)→ 发出警告,跳过,继续
- 如果目标中已存在同名文件 → 将目标重命名为
<file>.<unix_ts>(避免覆盖) mv "$MEMORY_DIR_BASH/TRASH/<file>" "$CWD_BASH/_archive/trash-purged-$REPORT_DATE/<file>"
- 源:
- 永远不要使用
rm——即使在这里。文件被移动到_archive/,用户自行决定何时最终删除。
每个项目之后——在聊天中简短记录:✓ M1 applied (3 files merged → project_pikabu_mcp.md)。
出错时(读取失败、编辑冲突、文件缺失)——不要停止,添加到 failed[] 并附上 reason,然后继续。
阶段 5 — 验证并报告
echo "=== Memory dir state ==="
wc -l "$MEMORY_DIR_BASH/MEMORY.md"
ls "$MEMORY_DIR_BASH"/*.md | wc -l
ls "$MEMORY_DIR_BASH/TRASH/" 2>/dev/null | wc -l
# 检查 MEMORY.md 中的断开链接(Python — 仅使用 _WIN 路径;bash-mount 的 /c/... Windows 上的 Python 无法理解)
python -c "
import re, os
content = open(r'$MEMORY_DIR_WIN/MEMORY.md', encoding='utf-8').read()
links = re.findall(r'\(([^)]+\.md)\)', content)
for link in links:
full = os.path.join(r'$MEMORY_DIR_WIN', link)
if not os.path.isfile(full):
print(f'BROKEN: {link}')
"
如果存在 BROKEN——添加最终自动修复:编辑 MEMORY.md,删除断开的链接,报告更改。
...
[已截断以进行翻译]