关于 kk-curate
kk-curate 是用于整理 Kenkeep 会话日志提案的技能,可批量审核候选内容并更新项目知识库。它通过 CLI 去重,并交由用户交互处理发现的矛盾。
主要功能
- 01从会话日志提取提案
- 02批量审核候选内容
- 03新增、修改、标记矛盾或丢弃
- 04通过 CLI 对操作去重
- 05重新生成知识索引
- 06与用户共同解决冲突
适用场景
- 01整理会话中捕获的知识
- 02清理重复或低价值提案
- 03审查项目知识中的矛盾
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ e0ipso/ kenkeep --skill kk-curate安装后在 Claude Code 中输入/kk-curate确认已加载。
附带文件batch-agent-prompt.md
详细介绍
kk-curate
你是整理者。阅读当前会话中的待处理会话日志,为每个候选项决定操作,通过 CLI 原语执行一次去重,然后使用 curate-persist 持久化保留的操作,重新生成索引,并与用户交互解决发现的矛盾。没有子 Agent,也没有运行器——你就是负责整理的大模型。
无操作也是正确的结果。 如果所有候选项都是改述、信号较弱或已有内容涵盖,正确结果就是不写入任何内容,并保持现有节点不变。切勿为了证明本次运行有必要而凭空修改内容;请直接报告“没有更改;知识库已是最新状态”。
确定项目根目录
使用随附的检测器确定仓库根目录(包含 .ai/kenkeep 的目录),然后将输出的路径作为以下所有命令的工作目录:
KK_REPO_ROOT=$(node .ai/kenkeep/scripts/kk-detect-root.mjs) || exit $?
cd "$KK_REPO_ROOT" || exit $?
pwd
0. 从待处理的会话日志中提取提案
在开始整理之前,在本会话中直接从 proposal_status: pending 的会话日志里提取提案。
-
列出待处理的会话日志。 使用
Glob(或ls)列出.ai/kenkeep/_sessions/*.md。读取每个文件的 frontmatter,筛选出proposal_status: pending的文件。按captured_at升序排列。 -
提前结束。 如果没有待处理项,则不发送消息,直接进入步骤 1。
-
加载提取提示词。 首先读取
.ai/kenkeep/.config/prompts/proposal-extract.md(仓库级覆盖文件)。如果该文件不存在,则读取随 npm 包附带的模板templates/prompts/proposal-extract.md(相对于已安装的 npm 包)。遵循提示词中的提取规则——不要在此处嵌入副本。 -
按顺序逐个处理待处理的日志(按
captured_at排序)。某个日志处理失败,不应中止其余日志: a. 完整读取文件。 b. 提取转录部分(## Transcript与## Proposal之间的内容)。 c. 遵循提示词中的提取规则,生成符合ProposalOutputSchema的 JSON 对象:{ "practice": [...], "map": [...] },其中每个条目都包含{ type, tags, title, description, body, kk_confidence }。 d. 将 JSON 传入 CLI 原语:echo '<json>' | npx kenkeep@latest session-log update-proposals <path> --status donee. 如果失败(输出格式错误、违反 schema 或 CLI 错误),调用:
npx kenkeep@latest session-log update-proposals <path> --status failed --error "<message>" -
至少处理一条日志时,报告摘要:
Extracted proposals from N session(s) (M failed).(将 N 和 M 替换为实际数量)。 -
继续步骤 1。
1. 列出待处理的会话日志
使用 Glob(或 ls)列出 .ai/kenkeep/sessions/*.md。读取每个文件的 frontmatter,仅保留同时满足以下条件的文件:
proposal_status: done,且curator_processed_at未设置(没有此键,或值为空字符串)。
将符合条件的文件按 captured_at 升序排列。这是规范顺序——必须保持此顺序。
提前结束。 如果没有符合条件的文件,只打印一行,然后停止(跳过以下所有步骤):
No pending session logs to curate. Nothing to do.
2. 分批读取会话并起草整理操作,每批最多 10 条
一次提供过多上下文会导致输出质量下降,因此请分批处理。每批最多处理 10 条会话。将按顺序排列的待处理会话划分为连续的批次,每批不超过 10 条(保持 captured_at 顺序)。批次编号为 1..N。
预先生成一次运行 ID,供每批临时文件和步骤 3 的提案文件复用:
RUN_ID=$(uuidgen 2>/dev/null || date -u +"curate-%Y%m%dT%H%M%SZ")
mkdir -p .ai/kenkeep/_logs/curator
选择路径:并行分派子 Agent,还是按顺序内联处理
探测可用工具,并根据共享附录 .ai/kenkeep/.config/prompts/sub-agent-delegation.md 选择并行或内联路径(探测定义、每轮最多 5 个并发任务及波次规则、绝对草稿路径,以及已发出/已验证/无效工件的形状)。
并行路径(首选,适用时)
并行单位是一批 ≤10 个会话(<N> 从 1 开始编号)。对于当前波次中的每批(每个协调器轮次 ≤5 批):
-
计算其绝对草稿路径,并在委派前追加
issued行:N=1 # batch index DRAFT_PATH="$(pwd)/.ai/kenkeep/_logs/curator/${RUN_ID}__${N}.draft.json" echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"issued\",\"runId\":\"${RUN_ID}\",\"batchN\":${N},\"sessions\":<count>}" \ >> .ai/kenkeep/_logs/curator/${RUN_ID}__${N}.jsonl -
使用同级文件
batch-agent-prompt.md中的提示词,为该批次派发一个子 Agent,将<list>替换为会话文件的绝对路径,将<DRAFT_PATH>替换为上面的DRAFT_PATH。该 Agent 会将其CuratorAction数组(根据curator-output验证)写入该路径。
该波次中的所有子 Agent 返回后,收集器轮次使用确定性原语汇总并验证各批次草稿,而不是手动拼接:
PROPOSALS=$(mktemp -t kk-curate-proposals.XXXXXX.json)
npx --yes kenkeep@latest drafts collect --run-id "$RUN_ID" --schema curator-output > "$PROPOSALS"
drafts collect 会读取每个 ${RUN_ID}__*.draft.json,根据 curator-output 验证每个批次,将通过验证的内容拼接为 JSON 数组并输出到标准输出(捕获到 $PROPOSALS),向每个批次的 .jsonl 追加 validated/invalid 事件,并将跳过的批次报告到标准错误——不会因单个批次出错而中止。随后 $PROPOSALS 即可用于第 4 步;跳过第 3 步的重新生成。第 4 步中的单次 curate-dedup 调用会对所有通过验证的批次的操作一并运行——与当前行为相同。
内联路径(备用)
如果没有可用的子 Agent 派发原语,则在此会话中按顺序起草——即已发布的行为。对每批:
- 完整读取该批中的每个会话文件。
- 每个会话的 frontmatter 中的
proposals:块包含practice: [...]和map: [...]数组。每个条目包含{ type, tags, title, description, body, kk_confidence }。 - 按顺序处理每个候选项(practice 和 map),决定采取哪种操作:add、modify、contradict 或 drop。使用下方规则。使用
candidate_origin = "<session_id>:<practice|map>:<index>",其中<index>是该数组中从 0 开始的位置。 - 构建操作对象(架构见下文),并追加到当前会话中所有批次的操作列表。
持续累积,直到处理完所有批次。
操作规则
add — 新知识
当候选项确实是新的,并且没有充分迹象表明现有知识库节点已涵盖相同范围时使用。如果候选项似乎与现有节点重叠,优先选择 drop 而非 add。
适合新增的迹象:
- 该主题对知识库而言是新的。
- 候选项包含独特内容(理由、范围、示例),其他位置没有。
- 现有关联节点讨论的是相邻主题,而非该主题本身。
包装器会根据标题生成 slug,并在磁盘上发生冲突时自动添加后缀(-2、-3……),但如果你察觉到真正的重叠,优先选择 drop(或者在候选项完善了现有节点时选择 modify)。
add 还需要指定一个所属分支:nodes/ 下叶节点所在的现有文件夹。你应在设置 kk_relates_to / kk_depends_on 的同一轮推理中选定该文件夹(见下文“关联与放置”),并将其记录在操作的 home_folder 字段中。将 home_folder 留空、设为 null 或空字符串,叶节点就会放在 nodes/ 根目录下(根目录回退位置)。
modify — 优化现有节点
当现有节点已涵盖此主题,但候选内容在不否定它的前提下对其进行扩展或优化时使用。
以下迹象表明适合进行修改:
- 现有节点与候选内容的范围相同(相同的约定、模块或易错点),但候选内容补充了更新后的示例、更多理由、新支持的情况、缺失的细节或澄清。
- 两者相容(可以同时成立)。
- 相较于现有正文,候选内容确实带来了新信息,而不只是换一种说法。
修改会按 ID 在现有路径原位覆盖现有叶节点(nodes/<...>/<target_node_id>.md,无论它目前位于树中的何处),会将内容合并到其中;它绝不会移动该叶节点,也不会设置 home_folder。必须提供 target_node_id,且该 ID 必须已存在于磁盘上;如果不存在,持久化步骤(node write)会改为创建新节点,这并非 modify 的意图。因此,发出 modify 操作前,必须通过对 nodes/ 执行 Glob(或读取相关分支的 index.md)确认目标存在。
最终状态改写规则。 合并后的正文应以现在时描述当前状态。绝不要追加“以前……”或“过去曾经……”之类的段落,也不要在正文中叙述“项目从 X 转变为 Y”。当新候选内容描述的是变更过程时,应改写现有节点正文,只保留新的最终状态。知识库记录的是项目当前状态,而不是变更日志。
重要: 如果候选内容与现有节点实质相同,只是换了种说法,则应直接舍弃。修改必须带来真正的新信息。
修改克制原则。 遵循 .ai/kenkeep/.config/prompts/knowledge-admission.md 中统一的修改克制标准(这些规则的唯一来源):优先进行最小幅度的原位编辑,而非重写;绝不无谓改动准确的节点。每项编辑都必须对应到引发该编辑的具体提案或冲突(影响计划式说明:源变更 → 受影响节点 → 所需编辑 → 原因);绝不对现有节点进行纯格式调整。若 modify 只会润色文字,则应舍弃。
contradict — 否定现有节点
当候选节点直接否定一个有效的现有节点时使用(两者不能在同一范围内同时为真)。用户之后会在会话中解决冲突。
判断确实存在矛盾的迹象:
- 现有节点写着“始终执行 X”或“针对情况 Y 执行 X”;候选节点则写着“绝不执行 X”或“针对情况 Y 不执行 X”。
- 用户在产生此候选节点的会话中明确推翻了先前的决定。
- 候选节点的范围与现有节点的范围完全重叠,而非部分重叠。
**重要提示:**如果候选节点的范围是现有节点的子集或例外,就不构成矛盾;这属于新增内容(或修改),应使用 kk_relates_to。例如:现有节点写着“使用默认缓存标签”,候选节点写着“对于个性化页面,改用每用户缓存上下文”,两者可以同时为真——应提交 add,并设置 kk_relates_to: [<existing node id>],而不是 contradict。
矛盾操作不会修改任何节点文件。去重原语会将冲突写入 .ai/kenkeep/conflicts/<id>.md;冲突解决流程(下文第 7 步)会逐个检查文件并询问用户。请确保 proposed_node 和 rationale 足够完整,让用户无需重新运行你也能作出决定。
**选择 target_node_id。**指向候选节点所否定的那个现有节点。如果两个现有节点都与候选节点重叠,应选择范围最贴合的那个,并在 rationale 中提及另一个;不要针对同一个候选节点提交两项 contradict 操作。
**撰写 rationale。**用一到两句话说明候选节点否定了哪个现有主张,以及两者为何不能同时成立。审阅者会先看这里。
最终状态的正文。proposed_node.body 仅描述新的最终状态,并使用现在时。只阅读新节点正文的审阅者,应能看到当前规则,就像它一直以来都是如此。
drop — 不作更改
当候选节点不应导致任何更改时使用。丢弃的原因包括:
- 只是对现有节点近似改写,没有提供新信息。
kk_confidence较低,且内容含糊。- 候选节点记录的是通用编程知识,而非项目专属知识。
- 候选节点内部自相矛盾,或提及了其他地方不存在的事物。
- 以变更为中心的表述——转型叙述、迁移过程、重命名或删除记录、“我们以前做 X,现在改为做 Y”之类的措辞。无论置信度如何,都应自动丢弃。知识库描述的是项目当前的最终状态,而不是项目历史。
- 共享知识准入标准排除的任何内容——维护/生命周期操作、项目故事或历史(尤其是计划/工单/问题引用),以及包装成实践的一次性偶发事实。请遵循
.ai/kenkeep/.config/prompts/knowledge-admission.md(其中也包含六个月保留测试和提炼规则);这些内容一律自动丢弃。 - 候选节点正文或描述中的非有效来源信号:
- 保留意见/试探性措辞(“我们或许可以”“我们可以考虑”“可能”“这个想法是”)。实践节点描述的是规则,而非假设。
- 提及假设或尚未实现的实体(“计划中的 X”“等我们添加 Z 后”)。映射节点描述的是实际存在的内容。
- 以计划或任务为范围的表述(“对于此计划,我们将……” “成功标准是……”)。
kk_confidence较低、没有理由说明且没有具体示例。
请综合权衡这些信号;当它们共同表明会话没有产生有效成果时,就丢弃。单一信号不应自动触发丢弃。
**提炼规则。**请遵循 .ai/kenkeep/.config/prompts/knowledge-admission.md 中的提炼规则和保留测试:当候选节点叙述了转型、维护操作或项目故事,但也传达了清晰且持久的原则或当前状态事实时,应提取其中持久的部分,并通过 add 或 modify 保留(改写为长期适用的规则或现在时事实);如果候选节点完全是在叙述过程,则丢弃。
关联与放置
知识库是 nodes/ 下按主题嵌套的文件夹树:包括一个根索引节点、分支索引节点,以及任意深度的叶节点。每次执行 add 时,都要进行一次推理,同时生成两个输出:交叉边和归属分支。不要进行第二次推理。
- 沿树向下查找。 从根索引节点(
nodes/index.md)开始,沿着与候选内容摘要相关的分支索引节点向下查找。索引节点会列出其子文件夹和叶节点,因此你可以像发现过程一样,逐步找到最接近的现有笔记。 - 设置交叉边。 从最近的现有叶节点出发,按 ID 设置
kk_relates_to(若某个节点确实依赖于另一个节点,也设置kk_depends_on)。边通过 ID 解析,与叶节点所在位置无关。特别是对于 map 节点,优先设置指向负责管理如何更改该实体的 practice 节点的kk_relates_to边——关于“编辑时需要注意”的知识就在这些 practice 节点中,而这条边能让 Agent 从 map 节点找到这些知识。如果候选内容或其会话记录提供了具体的编辑时检查项,也要在 map 正文中保留一条简短的可选“更改此项时,请验证……”说明;不要为了填充模板而凭空编造检查项。 - 评定归属分支。 根据同一次向下查找,按各索引节点(文件夹)的子树与候选内容主题的匹配程度排序,并选出最匹配的现有文件夹。将其记录在操作的
home_folder中(相对于nodes/的主题路径,例如cli或knowledge-base/index)。身份由 ID 确定,绝不取决于所选文件夹。 - 回退到根目录。 如果没有现有文件夹达到相关性标准,则将
home_folder留空、设为 null 或空字符串。随后写入器会将叶节点放在nodes/根目录下。这是有意且明确的结果,并非错误;之后的再平衡流程会将其迁移。切勿为了避免放入根目录而勉强匹配不相关的文件夹。
modify、contradict 和 drop 绝不设置 home_folder。modify(去重更新)会根据 ID 在现有叶节点当前路径下原地重写,不接受文件夹参数,也不会迁移节点。
约束(适用于每项操作)
- 绝不跨越 practice/map 边界。 practice 候选内容不得变为 map 节点,反之亦然。
- 绝不覆盖无关节点。
modify必须针对范围确实与候选内容匹配的节点;否则优先选择add(带有kk_relates_to)或contradict。 - 保持谨慎。 不确定该选 add 还是 modify 时,优先选择 modify(减少重复)。不确定该选 modify 还是 drop 时,优先选择 drop(减少噪声)。
- 整理时绝不更改树结构。 整理步骤(起草并持久化叶节点,即步骤 2 到 6)会将叶节点放入现有文件夹;绝不会创建、拆分或合并文件夹或分支。整理过程中唯一可能产生的结构性结果是回退到根目录(将叶节点放在
nodes/根目录下)。结构性更改只会在最终再平衡阶段(步骤 6b)进行,且只有在确定性触发条件满足时才会进行。
操作对象架构
每个操作都符合 CuratorActionSchema;操作数组就是 curator-output 合约。请从 CLI 获取准确结构,而不要自行推导,并在去重前进行验证:
npx --yes kenkeep@latest schema curator-output会输出 JSON Schema(包括操作对象及其嵌套的proposed_node)。- 组装好
$PROPOSALS后,运行npx --yes kenkeep@latest validate curator-output "$PROPOSALS";如果退出码非零,请读取错误中引用的路径,修复有问题的操作,然后重新验证,直至通过。
上述操作语义仍由你来应用:选择哪种操作(add/modify/contradict/drop)、最终状态重写规则、最小范围的矛盾处理,以及 home_folder 的放置规则——只有 add 设置 home_folder;modify、contradict 和 drop 不包含该字段,而只有 drop 的 proposed_node 为 null。其余部分由架构强制执行,包括拒绝任何未知的 proposed_node 键。
3. 写入提案临时文件
$RUN_ID 已在步骤 2 开始时生成,并在此处复用。生成 $SURVIVORS;如果步骤 2 的收集器尚未生成并填充 $PROPOSALS,也生成 $PROPOSALS:
SURVIVORS=$(mktemp -t kk-curate-survivors.XXXXXX.json)
# Only run the next two lines if you came through the inline path:
PROPOSALS=$(mktemp -t kk-curate-proposals.XXXXXX.json)
# Then Write your accumulated actions array (JSON array, top-level) to $PROPOSALS.
如果你采用了并行路径,$PROPOSALS 已包含拼接后且通过架构验证的操作数组(drafts collect 已完成验证)——跳到步骤 4。如果你采用了内联路径,现在将累积的操作数组(顶层为 JSON 数组)Write 到 $PROPOSALS,然后进行验证:npx --yes kenkeep@latest validate curator-output "$PROPOSALS"。修复所有错误中引用的路径,并重新验证,直至通过,然后再进入步骤 4。
4. 通过原语去重并添加标记
调用 curate-dedup:
npx --yes kenkeep@latest curate-dedup \
--input "$PROPOSALS" --output "$SURVIVORS" --run-id "$RUN_ID"
去重范围涵盖整棵树:系统会读取 nodes/ 下所有文件夹中的现有叶节点(不限深度),因此无论重复项当前位于何处,都会被匹配。其行为与扁平空间相同,区别仅在于搜索范围覆盖整棵树。重复项会按 id 在现有叶节点的当前路径就地更新(执行 modify),不会移动位置。
此单次调用会原子地完成以下操作:
- 对操作进行去重(跨批次的重叠项会合并;保留置信度较高的一项)。去重后保留的
add会原样保留其home_folder。 - 为每个保留的
contradict操作生成${RUN_ID}-N冲突 id,并写入.ai/kenkeep/conflicts/<id>.md文件。 - 为其处理的每个待处理会话日志添加
curator_processed_at/curator_run_id标记。 - 将未发生冲突的保留项(仍需作为节点持久化的操作)写入
$SURVIVORS。
它会在标准输出中打印一行 JSON:
{"kept":N,"conflicts":M,"stamped":K,"runId":"..."}
记录这些数字并报告给用户。
5. 通过 curate-persist 持久化保留的操作
使用确定性原语一次性持久化 $SURVIVORS——该原语与 kk-session-extract 使用的相同——不要自行编写 node write 循环:
npx --yes kenkeep@latest curate-persist --input "$SURVIVORS"
curate-persist 会根据 curator-output 合约验证保留项,然后对每个操作,通过共享节点写入器写入 add / modify(add 写入其 home_folder;如果未指定,则回退到 nodes/ 根目录;modify 则根据 id,在目标当前路径就地重写,不会移动位置),跳过 drop,并拒绝 contradict(冲突由 curate-dedup 处理)。它会在标准输出中打印一份 JSON 摘要(包括 written、dropped、failed 以及包含解析后 id 和放置位置的逐操作 results),仅当输入格式错误或至少一个有效操作失败时才以非零状态码退出——无论哪种情况,成功的写入都会保留。
记录摘要:在步骤 7 中,报告每个叶节点的放置位置(home_folder 或 root fallback)以及每个操作的失败情况。如果某个 modify 的 target_node_id 在磁盘上不存在,结果中会将其列为失败操作——请明确指出,让用户知道更新未能写入。
6. 重建索引
所有写入完成后:
npx --yes kenkeep@latest index rebuild
6b. 重新平衡(最终阶段,执行并折叠)
这是整理流程的最后阶段,也是唯一会更改树结构的阶段。在此处折叠,不需要第二条命令,也不需要再次提示。请在写入叶节点并重建索引(步骤 6)后、生成报告前运行。
6b.1 运行确定性触发器
该触发器具有确定性且不使用 LLM:它读取 Plan 1 中每个文件夹的占用率/标签多样性/叶节点大小指标,应用由滞后机制门控的决策规则,并打印稳定的 JSON 决策。运行触发器并捕获标准输出:
npx --yes kenkeep@latest rebalance trigger
它会且只会打印一行 JSON:
{"actions":[{"branch":"<path>","operation":"<split-folder|split-leaf|merge|create-branch>"}, ...]}
跳过路径(零额外成本)。 如果 actions 为空({"actions":[]}),则树结构已平衡,超出滞后边界。不要进入 LLM 聚类步骤。请在步骤 7 的摘要中记录“重新平衡:无结构性操作”,然后继续执行步骤 7。这是常见情况;大多数整理流程不会触发任何操作,最终结果与当前流程完全相同。
执行路径。 如果 actions 非空,则继续执行 6b.2。只针对触发器指定的分支进行判断,绝不要扩大范围。
6b.2 仅对受影响的分支提出结构操作
对于 actions 中的每个条目,只读取该分支(指定文件夹的 index.md 及其叶节点,或者 split-leaf / create-branch 指定的叶节点),并决定一项具体操作。这是整个运行中唯一的非确定性步骤;它被隔离在确定性触发条件和人工提交关卡之后。不要触碰触发条件未指定的任何分支。
将每种操作映射为一个具体的计划条目。对于操作创建的每个新文件夹,还要编写一行文件夹 summary:一个名词短语/句子片段,用来补全 for more information on <summary>(以小写字母开头,不以句号结尾,简洁)。摘要应以任务为关键,而不只是描述结构:先说明文件夹中包含什么,再附加简短的 ; read when <task pattern> 子句,指出哪些任务应触发深入查阅(例如 the five harness adapters and their isolation rules; read before adding a harness or changing hook wiring)。Agent 会将自身任务与这些摘要匹配来确定路由,因此触发子句是确保可靠深入查阅的关键。移动操作会将其写入新文件夹的文件夹摘要 sidecar,之后每次确定性重建都会保留它;父级索引会将它拼接到 Load … 深入查阅指引中。
- split-folder(
branch是条目过多的文件夹):将该文件夹的直接叶节点按主题分成两个或更多子文件夹。输出{"operation":"split-folder","branch":"<folder>","groups":[{"subfolder":"<name>","summary":"<fragment>","ids":["<id>", ...]}, ...]}。每个 id 都必须是branch当前的直接叶节点;每个叶节点只能分配给一个子组;为每个子文件夹编写一个summary。 - merge(
branch是条目稀少/冗余的文件夹):选出最佳的现有目标文件夹into(主题涵盖该稀疏分支的同级文件夹或父文件夹;若目标是nodes/根目录,则使用空字符串)。输出{"operation":"merge","branch":"<folder>","into":"<destination>"}。合并不会创建文件夹,因此无需编写summary;目标文件夹保留其自身已持久化的摘要。 - create-branch(
branch是没有归属的根叶节点,属于新颖的顶层主题):选择一个新的顶层文件夹名称,并确定属于该文件夹的叶节点。输出{"operation":"create-branch","folder":"<new-top-level>","summary":"<fragment>","ids":["<id>", ...]}。 - split-leaf(
branch是一个内容臃肿、涵盖两个或更多概念的叶节点):将其拆分为两个或更多新子文档,并放入以该叶节点命名的文件夹中。输出{"operation":"split-leaf","leafId":"<old-id>","folder":"<folder>","summary":"<fragment>","children":[{"title":"...","summary":"...","body":"...","tags":["..."],"relates_to":["..."]}, ...]},且至少包含两个子项。顶层summary描述新文件夹;每个子项都有自己的叶节点summary。该操作会生成新 id,并记录从旧 id 出发的重定向;不要编写 id。
将所有条目组装成一个操作计划:{"operations":[ ... ]}。将其写入 tmpfile:
REBAL_PLAN=$(mktemp -t kk-rebalance-plan.XXXXXX.json)
# Write your {"operations":[...]} plan to $REBAL_PLAN.
6b.3 确定性地应用移动操作
将计划交给确定性移动操作。它会以内容字节稳定、id 稳定的 git 重命名方式应用每项移动(split-leaf 会生成新 id 并添加重定向),然后对受影响的索引节点和 nodes_hash 执行确定性重建。不要手动移动文件或重新生成索引。
npx --yes kenkeep@latest rebalance move --input "$REBAL_PLAN"
它会输出一行 JSON,即你需要带入步骤 7 的结构摘要:
{"moves":[{"operation":"...","id":"...","from":"...","to":"...","newIds":["..."],"redirectFrom":"..."}, ...]}
捕获该内容。不要提交、暂存或还原任何内容:结构移动和整理后的叶节点写入现在都位于同一个未提交的工作区差异中。人工通过 git commit 接受;通过按路径指定的 git restore 仅拒绝结构移动。
7. 报告摘要,然后处理冲突
向用户报告关键数字(kept、conflicts、stamped、runId)、写入的节点数,以及丢弃项数。还要列出每个已写入叶节点的放置决策:对于每个已持久化的 add,报告其 id 和所在文件夹(所选的 home_folder;若未选择,则报告 root fallback);对于每个 modify,说明它已在当前路径原地更新。这样人类可以同时审核放置位置和内容。
无操作也是正确结果。 当 nodes_written == 0(所有候选项都被丢弃)时,知识库已是最新状态——请明确说明“没有更改;知识库已是最新状态”,不要为写入集为空而道歉。如果没有任何提议包含真正的新信息,写入集为空就是首选结果。
结构摘要(重新平衡)。 然后打印重新平衡阶段(步骤 6b)的结构摘要,与上面的内容摘要分开显示,并额外提供,让人类了解结构差异:
- 如果 6b.1 报告无需操作,则打印一行:
Rebalance: no structural action (tree balanced). - 否则,对于
{"moves":[...]}摘要中的每个移动,打印一行,注明操作和受影响的分支:split-folder/merge/create-branch显示<id>: <from> -> <to>;split-leaf显示<old-id> -> <new-id>, <new-id>, ... (redirect recorded)。最后附上:Review the structural diff with \git diff --summary` (R entries are renames); accept by `git commit`, reject just the structural moves with a path-scoped `git restore`.`
如果 conflicts == 0,打印放置行、结构摘要,然后打印且仅打印一行摘要,随后停止:
Curated <nodes_written> nodes; <drops> dropped; no conflicts. Review the written files under .ai/kenkeep/nodes/.
否则,继续执行步骤 7a。
7a. 准备待处理冲突
运行一次确定性原语,以获取按展示顺序排列的待处理冲突,每个冲突都包含计算得出的默认回复:
npx --yes kenkeep@latest conflict prepare
它会读取待处理的冲突文件,对其排序和分组(按 target_node_id 排序,null 放在最后,然后按 proposed_kind、detected_at 排序;连续且共享非 null target_node_id 的冲突会组成一组),根据差异比率规则计算每个冲突的默认回复,并打印 {"count":N,"conflicts":[...]}。每个冲突包含 id、target_node_id、proposed_title、proposed_confidence、rationale、proposed_body、group_id、first_in_group、已解析的 existing 节点(每组仅在 first_in_group 中渲染一次),以及建议的 default(y/n/s)。按给定顺序遍历 conflicts;默认值仅为建议,并非最终判定。
7b. 展示每个冲突
对于准备列表中的每个冲突:
- 如果
first_in_group为真且existing非null,则展示现有节点的title、description和相关正文摘录,每组仅展示一次。 - 简洁展示提议的矛盾内容:
proposed_title、proposed_confidence、rationale和proposed_body。
(当没有现有节点可用于比较差异时,s 是安全的默认值;该原语已经对此进行了编码。)
7d. 询问用户并解析回复
询问用户,并突出显示默认值,例如:
Accept this proposal? [Y/n/s/k] (default: Y)
将默认字母大写,并放在方括号组中,使其醒目。
按以下规则解析回复:
- 空回复、
y、Y、yes→ 选择y。 n、N、no→ 选择n。s、S、skip→ 选择s。k、K、keep→ 选择k。- 其他任何内容 → 使用相同的默认高亮方式,重新询问同一个冲突。不要从“看起来不错”“好的,请继续”或“跳过这个”等文字中推断意图;必须要求用户输入列出的某个标记。空回复表示采用默认值。
7e. 应用结果
将所选回复映射为操作:
y(接受提议):在target_node_id节点当前路径上原地重写该节点——使用Globnodes/**/<target_node_id>.md定位它(位置按主题确定)——写入提议的正文和 frontmatter(将现有target_node_id用作 slug,通过node write更新同一 id 对应的节点;如果已完整整理好 frontmatter,也可以直接对解析出的路径使用Write),然后执行rm .ai/kenkeep/conflicts/<id>.md。n(拒绝提议):执行rm .ai/kenkeep/conflicts/<id>.md。现有节点保持不变。s(跳过):保留冲突文件不动。下次 curate 时它会再次出现,且status: pending保持不变。不要编辑或删除该文件。k(留作记录):将冲突文件保留在磁盘上,作为供日后审阅的历史记录。请少用此选项。现有节点保持不变。
对一组中的每个冲突作出决定后,再进入下一组。
8. 交接
请告知用户检查 .ai/kenkeep/ 下已更改的节点和冲突文件。第 6 步已刷新 ENTRY.md 和 GRAPH.md(如果 rebalance 阶段执行了操作,rebalance move 原语也会再次刷新它们)。第 6b 步中的任何结构调整都包含在同一个未提交差异中;用户可以通过 git commit 接受所有更改,或使用按路径指定的 git restore 仅撤销结构调整。
约束
- 冲突解决的回复格式严格限定为
y/n/s/k(或其完整形式/留空以采用默认选项)。不要接受改写措辞的回复;应重新提示用户。 - 如果没有待处理的会话日志,请在第 1 步直接结束,并显示单行消息。不要调用任何原语。
- 如果
.ai/kenkeep/conflicts/为空,或所有文件的status都不是pending,则无需解决任何冲突;第 7 步的快速路径消息已涵盖这种情况。 - Rebalance(第 6b 步)仅作为 curate 的最后阶段运行;它不是独立命令或提示。当
rebalance trigger返回{"actions":[]}时,完全跳过 LLM 聚类步骤(不产生额外成本),并报告未执行结构调整。当触发操作时,只分析它指定的分支,绝不可扩大范围,并且只能通过rebalance move原语应用移动。绝不可手动移动文件或重新生成索引,也绝不可执行git add、git commit或git restore。 - dedup 原语不会加锁,并且在全新的
runId上具有幂等性——但不要对同一个$PROPOSALS使用不同的runId再次运行它;这会重复标记已消费的会话,并重复写入冲突文件。每个会话只调用一次curate-dedup。