Claude 会把用户需求翻译成自己的 TodoWrite,翻译那一步就已经丢东西了: 「修复 apple-header sticky」是施工步骤,不是原始需求里的功能要点。 再拿它自己的 todo 去验收,等于让它自己判自己的卷子。 清单挂在项目上跨会话累积,基线取自用户原话和用户批准过的计划。 几个刻意的设计: - Evidence 在类型层面就分开 claimed / diff / ran。全塞进 [String] 等于在 数据结构上承认「它说改了」和「它真改了」是一回事,而这正是要解决的问题。 汇报条的「已证」只数 isProof 的,想把自述渲染成已验证得先改类型。 - 拦截的「上膛」只有 UserPromptSubmit 能做,Stop 拦过立即卸膛。Claude 没有任何路径能给自己上膛,所以「拦一次 → 它回一轮 → 又拦」这个死循环 是构造不出来的,不是被 if 挡住的。 - applyAudit 跳过用户终裁过的项。勾了「接受」一转身被机器打回 disputed, 那个勾就白点了。 - extract 区分「失败」(nil) 和「没有新要点」([])。混在一起的话模型抽风 一次就会清掉原话缓冲,而那是这个功能唯一的基线来源。 hook 从 5 类补到 9 类。PostToolUse 是最大的收获:Write/Edit 之后 Hub 第一手知道哪个文件被改了,不用回头问 Claude——问这个问题本身就给了 转述失真的机会。 实测确认(都不是推断): - hubctl hook userprompt 的 stdout 是 0 字节。这条必须验,因为 UserPromptSubmit 的 stdout 会被当成附加 context 注入进对话。 - 项目级和全局 hook:命令字符串相同时 Claude Code 自己去重,不同才叠加。 第一次实验用了两条不同命令,据此普遍化成「一定会重复」是错的。 HookDedup 保留为深度防御(路径不一致时才真用得上),不是必需品。 - 真实拆解链路 20 秒出结果,正确忽略了缓冲里的闲聊、正确去重了已有条目。 ensure-project-config.sh 接了三个调用点。其中 TerminalDispatch.open() 是 关键:app 从岛/主窗口启动项目完全不经过 project-menu.sh,漏了那里等于 最常用的入口一个都没检查。 326 个测试。上膛、自述不算证明、外部编辑不被覆盖三条做过变异验证。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
53 lines
2.3 KiB
Swift
53 lines
2.3 KiB
Swift
import Foundation
|
|
|
|
/// 处理模型回复的公共部分。
|
|
///
|
|
/// 抽出来是因为「剥 ``` 围栏」这件事**每一个调模型的地方都要做**:
|
|
/// 实测即使提示词明说"只输出 JSON 对象",模型仍然会包一层 ```json … ```。
|
|
/// 不剥的话解析必然失败,那一整层 AI 能力等于完全没生效 —— 而且是静默失败。
|
|
///
|
|
/// 各写一份的话,某天有人在一处修了新的边界情况(比如围栏前面还有一句话),
|
|
/// 另一处不会跟着修。
|
|
public enum ModelOutput {
|
|
|
|
/// 剥掉 markdown 代码围栏,返回里面的内容。没有围栏就原样返回(已 trim)。
|
|
public static func stripCodeFence(_ raw: String) -> String {
|
|
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard text.hasPrefix("```") else { return text }
|
|
|
|
var lines = text.split(separator: "\n", omittingEmptySubsequences: false)
|
|
lines.removeFirst() // ```json 或 ```
|
|
if lines.last?.trimmingCharacters(in: .whitespaces).hasPrefix("```") == true {
|
|
lines.removeLast()
|
|
}
|
|
return lines.joined(separator: "\n")
|
|
}
|
|
|
|
/// 从模型回复里挖出一个 JSON 对象。
|
|
///
|
|
/// 比 `stripCodeFence` 更宽容:模型经常在 JSON 前后各写一段散文
|
|
/// ("好的,我核对了一遍:{…} 需要我继续吗?")。这时围栏剥不出东西,
|
|
/// 只能退回去找第一个 `{` 和最后一个 `}`。
|
|
///
|
|
/// **这不是过度设计。** 自查回答那条链路上,模型是在一段长对话的末尾
|
|
/// 被要求输出 JSON 的,它加几句客套话是常态而不是异常。
|
|
public static func extractJSONObject(_ raw: String) -> [String: Any]? {
|
|
let stripped = stripCodeFence(raw)
|
|
|
|
if let object = parseObject(stripped) { return object }
|
|
|
|
guard let start = stripped.firstIndex(of: "{"),
|
|
let end = stripped.lastIndex(of: "}"),
|
|
start < end
|
|
else { return nil }
|
|
return parseObject(String(stripped[start...end]))
|
|
}
|
|
|
|
private static func parseObject(_ text: String) -> [String: Any]? {
|
|
guard let data = text.data(using: .utf8),
|
|
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
else { return nil }
|
|
return object
|
|
}
|
|
}
|