@zyaiting/keelson 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -0
  3. package/README_CN.md +101 -0
  4. package/bin/keelson.js +15 -0
  5. package/hooks/codebuddy-session.mjs +67 -0
  6. package/hooks/opencode-session.mjs +65 -0
  7. package/hooks/prompt-state.mjs +66 -0
  8. package/hooks/session-start.mjs +94 -0
  9. package/package.json +64 -0
  10. package/registry/models.json +118 -0
  11. package/registry/platforms.json +92 -0
  12. package/skills/keelson/SKILL.md +44 -0
  13. package/skills/keelson/references/build.md +61 -0
  14. package/skills/keelson/references/context.md +34 -0
  15. package/skills/keelson/references/debug.md +46 -0
  16. package/skills/keelson/references/design-lenses.md +78 -0
  17. package/skills/keelson/references/discover.md +70 -0
  18. package/skills/keelson/references/engineer.md +110 -0
  19. package/skills/keelson/references/frontend-delivery.md +38 -0
  20. package/skills/keelson/references/frontend-interaction.md +31 -0
  21. package/skills/keelson/references/frontend-review.md +33 -0
  22. package/skills/keelson/references/frontend-visual.md +31 -0
  23. package/skills/keelson/references/frontend.md +33 -0
  24. package/skills/keelson/references/handoff.md +43 -0
  25. package/skills/keelson/references/harness.md +54 -0
  26. package/skills/keelson/references/interview.md +120 -0
  27. package/skills/keelson/references/land.md +47 -0
  28. package/skills/keelson/references/model.md +29 -0
  29. package/skills/keelson/references/plan.md +106 -0
  30. package/skills/keelson/references/reconcile.md +61 -0
  31. package/skills/keelson/references/shape.md +86 -0
  32. package/skills/keelson/references/verify.md +64 -0
  33. package/skills/keelson/templates/GLOSSARY.md +5 -0
  34. package/skills/keelson/templates/INTENT.md +22 -0
  35. package/skills/keelson/templates/NOW.md +9 -0
  36. package/skills/keelson/templates/README.md +60 -0
  37. package/skills/keelson/templates/ROADMAP.md +12 -0
  38. package/skills/keelson/templates/change-quick.md +16 -0
  39. package/skills/keelson/templates/change.md +32 -0
  40. package/skills/keelson/templates/delta-spec.md +12 -0
  41. package/skills/keelson/templates/handoff.md +27 -0
  42. package/skills/keelson/templates/ledger.md +3 -0
  43. package/skills/keelson/templates/resident-block.md +7 -0
  44. package/skills/keelson/templates/rules-general.md +10 -0
  45. package/skills/keelson/templates/rules-index.md +5 -0
  46. package/skills/keelson/templates/spec.md +14 -0
  47. package/skills/keelson/templates/tasks.md +9 -0
  48. package/skills/keelson/templates/workflow.md +18 -0
  49. package/skills/zh/keelson/SKILL.md +46 -0
  50. package/skills/zh/keelson/references/build.md +61 -0
  51. package/skills/zh/keelson/references/context.md +34 -0
  52. package/skills/zh/keelson/references/debug.md +46 -0
  53. package/skills/zh/keelson/references/design-lenses.md +78 -0
  54. package/skills/zh/keelson/references/discover.md +70 -0
  55. package/skills/zh/keelson/references/engineer.md +110 -0
  56. package/skills/zh/keelson/references/frontend-delivery.md +38 -0
  57. package/skills/zh/keelson/references/frontend-interaction.md +31 -0
  58. package/skills/zh/keelson/references/frontend-review.md +33 -0
  59. package/skills/zh/keelson/references/frontend-visual.md +31 -0
  60. package/skills/zh/keelson/references/frontend.md +33 -0
  61. package/skills/zh/keelson/references/handoff.md +43 -0
  62. package/skills/zh/keelson/references/harness.md +54 -0
  63. package/skills/zh/keelson/references/interview.md +120 -0
  64. package/skills/zh/keelson/references/land.md +47 -0
  65. package/skills/zh/keelson/references/model.md +29 -0
  66. package/skills/zh/keelson/references/plan.md +106 -0
  67. package/skills/zh/keelson/references/reconcile.md +61 -0
  68. package/skills/zh/keelson/references/shape.md +86 -0
  69. package/skills/zh/keelson/references/verify.md +64 -0
  70. package/skills/zh/keelson/templates/GLOSSARY.md +5 -0
  71. package/skills/zh/keelson/templates/INTENT.md +22 -0
  72. package/skills/zh/keelson/templates/NOW.md +9 -0
  73. package/skills/zh/keelson/templates/README.md +60 -0
  74. package/skills/zh/keelson/templates/ROADMAP.md +12 -0
  75. package/skills/zh/keelson/templates/change-quick.md +16 -0
  76. package/skills/zh/keelson/templates/change.md +32 -0
  77. package/skills/zh/keelson/templates/delta-spec.md +12 -0
  78. package/skills/zh/keelson/templates/handoff.md +27 -0
  79. package/skills/zh/keelson/templates/ledger.md +3 -0
  80. package/skills/zh/keelson/templates/resident-block.md +7 -0
  81. package/skills/zh/keelson/templates/rules-general.md +10 -0
  82. package/skills/zh/keelson/templates/rules-index.md +5 -0
  83. package/skills/zh/keelson/templates/spec.md +14 -0
  84. package/skills/zh/keelson/templates/tasks.md +9 -0
  85. package/skills/zh/keelson/templates/workflow.md +18 -0
  86. package/src/cli.js +87 -0
  87. package/src/commands/ablate.js +96 -0
  88. package/src/commands/ask.js +64 -0
  89. package/src/commands/attest.js +71 -0
  90. package/src/commands/check.js +127 -0
  91. package/src/commands/context.js +95 -0
  92. package/src/commands/design.js +63 -0
  93. package/src/commands/doctor.js +157 -0
  94. package/src/commands/focus.js +84 -0
  95. package/src/commands/guide.js +59 -0
  96. package/src/commands/handoff.js +41 -0
  97. package/src/commands/hook.js +23 -0
  98. package/src/commands/impact.js +58 -0
  99. package/src/commands/init.js +289 -0
  100. package/src/commands/land.js +258 -0
  101. package/src/commands/models.js +62 -0
  102. package/src/commands/new.js +70 -0
  103. package/src/commands/platforms.js +39 -0
  104. package/src/commands/retro.js +114 -0
  105. package/src/commands/status.js +115 -0
  106. package/src/commands/uninstall.js +30 -0
  107. package/src/commands/validate.js +117 -0
  108. package/src/lib/args.js +30 -0
  109. package/src/lib/changes.js +114 -0
  110. package/src/lib/check-activity.js +29 -0
  111. package/src/lib/config.js +102 -0
  112. package/src/lib/decisions.js +59 -0
  113. package/src/lib/evidence.js +127 -0
  114. package/src/lib/fs.js +126 -0
  115. package/src/lib/git.js +353 -0
  116. package/src/lib/glob.js +54 -0
  117. package/src/lib/health.js +113 -0
  118. package/src/lib/lifecycle.js +120 -0
  119. package/src/lib/maintenance.js +66 -0
  120. package/src/lib/markdown.js +438 -0
  121. package/src/lib/models.js +195 -0
  122. package/src/lib/out.js +13 -0
  123. package/src/lib/paths.js +82 -0
  124. package/src/lib/rules.js +27 -0
  125. package/src/lib/runtime-path.js +22 -0
  126. package/src/lib/session.js +100 -0
  127. package/src/lib/specs.js +345 -0
  128. package/src/lib/transaction.js +93 -0
  129. package/src/platforms/index.js +3 -0
  130. package/src/platforms/integration.js +384 -0
  131. package/src/platforms/registry.js +46 -0
  132. package/src/platforms/runtime.js +249 -0
@@ -0,0 +1,64 @@
1
+ # Verifying(验证)
2
+
3
+ Ready/完成是由长期 gate 与证据支撑的状态,而证据有两个会独立失效的属性:记录可能无效(从没跑过、跑在旧代码上、只跑了一部分),内容可能无效(跑了、通过了,却仍没检查负责人要的东西)。本参考两者都管。它不会随模型变强而变薄,因为它关乎的是世界,不是判断。
4
+
5
+
6
+ 界面工作按需加载 `frontend.md`;视觉与交互验收遵循 `frontend-review.md` 和 `frontend-delivery.md`。
7
+
8
+ ## 记录有效性:`keelson check --record`
9
+ <!-- keelson: id=verify.fresh | without: "应该能过"和"看起来对"取代了运行命令;最后一次改动之前的证据被当成当前的 | sunset: never -->
10
+
11
+ 声称完成前先审阅配置的命令,首次运行使用 `keelson check --trust --record "<claim>"`;相同命令后续无需再次传 trust。CLI 将签名 `ledger.jsonl` 和 `evidence/<digest>.log` 保存在变更中。ledger.md 是可读摘要,手写 Verify 不授权落地。
12
+
13
+ 记录同时绑定完整代码与契约指纹,包括验收和决策。status/land 拒绝过期、失败、不完整或本机不信任的证据。显式单条命令只形成部分证据,除非它恰好覆盖完整配置检查。最终检查前完成验收记录;之后编辑会使证据失效。门禁全部通过时,在已有授权内落地。本机签名不隔离同权限进程,也不证明模型身份。
14
+
15
+ 某项检查跑不了(环境缺失、服务不可用),就在 ledger 里记一条 `Note:`,并写进 `NOW.md → Blocked / uncertain`。部分验证按部分汇报;绝不向上取整。
16
+
17
+ `config.yaml → check` 里的条目可以是普通字符串,也可以是 `{name, command, kind}`;`kind` 是 `test`、`lint`、`typecheck`、`build`、`fitness`、`check` 之一。`fitness` 检查是变成了一条命令的架构或质量约束(依赖方向、接口兼容性、延迟预算)。机械证据就是这整组检查在当前树上全部通过。它是必要的,但从来不充分。
18
+
19
+ ## 内容有效性:证据覆盖了请求吗?
20
+ <!-- keelson: id=verify.content | without: 测试通过而需求仍未满足;被评审的是实施者的总结,而不是负责人的请求 | sunset: never -->
21
+
22
+ 回到 `change.md → Acceptance` 和原始请求,而不是你自己对它的总结。每个验收项,写明覆盖它的测试、命令、人工检查或评审,并且只在那项检查真正跑过之后打勾。delta spec 里的每条需求或 scenario,写明覆盖它的检查。没被覆盖的,要么现在检查,要么作为缺口写进 `NOW.md → Blocked / uncertain`。然后对照命中的 rules 和受影响的 specs 读 diff;违反 rule 就是缺陷,哪怕所有测试都绿。
23
+
24
+ 修 bug 时保留反向检查:把修复撤掉,回归测试必须失败。两种情况都通过的测试什么也证明不了。
25
+
26
+ 成形阶段真正触发了哪个风险镜头,就验证对应义务,而不是再跑一张通用清单:安全看相关负向/滥用场景,并发看重复/顺序/故障行为,兼容性看旧消费者或迁移覆盖,可访问性看受影响交互,性能看有测量依据的目标/基线。仓库已经能证明的就复用证据,不重复制造检查。
27
+
28
+ 涉及错误处理、清理或资源生命周期时,沿入口到返回的完整路径检查,包括清理之后的终结操作和通知。同时在多个阶段注入错误:早期错误不能阻止后续必需动作,晚期错误不能悄悄替换先前错误。检查执行过哪些动作、最终传播哪些错误,以及最终观察者和调用者各自看到的状态,包括原先已存在的外层状态。断言错误身份、数量及契约要求的顺序;只数回调次数不能证明错误传播和状态恢复正确。额外的鲁棒性探测应与负责人原本规定的验收标准分开记录。
29
+
30
+ ## 对“这个机制有收益”的主张做反事实验证
31
+ <!-- keelson: id=verify.counterfactual | without: 方案本身测试全绿,但没人验证新增 cache、queue、retry 层、抽象、模型阶段或 reviewer 是否真的产生了用来证明其复杂度合理的收益 | sunset: never -->
32
+
33
+ 普通产品行为不需要做消融。只有当某个机制的理由本身是经验性主张时才使用:性能、可靠性、成本、质量、安全余量或其他可测响应。
34
+
35
+ 按照 `engineer.md` 里的 baseline 验证:使用同一代表性 workload/环境,分别让 candidate enabled,以及 disabled/简化,再对照实验前声明的 response/threshold。可靠性机制要注入它声称能处理的故障;AI/Agent stage 要用同一批 eval case 对比有无该 stage。
36
+
37
+ 如果移除机制并没有让目标明显变差,它的必要性就没有证据支持:删除它,或者把结论记为 inconclusive,而不是因为“全绿”就永久保留复杂度。两个机制可能交互时,测试最小有用组合,不要迷信 one-at-a-time removal。
38
+
39
+ 稳定的 response threshold 能自动化时升级成 fitness check;带噪声的探索实验留在 evidence/ledger note,不变成永久 gate。
40
+
41
+ ## 测试可以改,但不能悄悄削弱
42
+ <!-- keelson: id=verify.no-silent-weakening | without: 靠删断言、跳用例做到"全绿",而完成报告里看不出这种削弱 | sunset: never -->
43
+
44
+ 需求变了就改测试,这很正常。删掉断言、跳过用例、放宽容差,或者用 mock 替换真实检查,是对验收标准的改动:它需要负责人的决定(或明确的授权),并作为一条 `Ruling:` 写进 ledger,写明削弱了什么、为什么。隐瞒它的完成报告是错的。
45
+
46
+ ## 陌生读者评审(spec 档)
47
+ <!-- keelson: id=verify.fresh-reader | without: 作者评审自己的工作;同一个盲点通过两次 | sunset: 连续 50 次陌生读者评审都没发现逐任务评审漏掉的东西时 -->
48
+
49
+ 派一个没看过对话的评审者(spec 变更层级 ≥ `deep`),给它原始请求、`change.md`、delta specs 和 diff。让它找:没有真实覆盖的验收项、需求缺口、rule 违反、有风险的假设、任何维护者会反对的地方。每条发现要么处理,要么记入 ledger。第二个代理的同意是信号,不是证明;被检查的是验收清单。
50
+
51
+ ## 完成报告
52
+
53
+ 告诉用户做了什么、证据是什么、还剩什么。形式:
54
+
55
+ ```
56
+ Done: offset pagination on /orders, pager in the table.
57
+ Evidence: `npm test -- orders` exit 0 (14 passed); `npm run lint` exit 0 · tree 5bcb829dae. Acceptance 3/3.
58
+ Open: none. `keelson land add-pagination` succeeded; durable behavior/decisions were folded.
59
+ ```
60
+
61
+ <!-- guided -->
62
+ ## 这些词意味着你还没验证
63
+ "应该"、"大概"、"看起来"、"我相信它能用"。要对状态写下这些词时,改成去运行命令。
64
+ <!-- /guided -->
@@ -0,0 +1,5 @@
1
+ # 术语表
2
+
3
+ 本项目的共同词汇:一行一个术语,写代码、specs 和对话共同使用的那个含义。当两个词开始指同一件事、或一个词开始指两件事时,添加一条。当某个术语在系统的另一部分含义不同时,点明那个部分,两个定义都保留。
4
+
5
+ - … — …
@@ -0,0 +1,22 @@
1
+ # {{project}}
2
+
3
+ ## Why this exists
4
+ 一段话。这个项目解决什么问题,为谁解决。如果这段话错了,建在它之上的一切都是错的。
5
+
6
+ ## Boundaries
7
+ - In scope: …
8
+ - Explicitly not: …(把那些诱人但已决定不做的事写下来)
9
+
10
+ ## Hard constraints
11
+ - …(运行时、兼容性、性能、安全、许可证)
12
+
13
+ ## Authorizations
14
+ 代理可以独自决定什么,什么必须带着推荐拿回来。
15
+ - Decides alone: 已确认范围内的局部实现选择;测试结构;遵循既有模式的命名。
16
+ - Recommends, owner decides: 任何改变用户可见行为、范围或长期承诺的事;新依赖;公共接口变化。
17
+ - Always confirms: 不可逆数据操作、生产修改、权限扩大、破坏兼容性。
18
+
19
+ ## Working defaults
20
+ - Change sizing: auto(由代理判断;单次请求可用"按 spec 处理"/"直接做"覆盖)
21
+ - Quick changes: proceed after write-back(写回理解后直接继续)
22
+ - Spec changes: wait for approval(等待批准)
@@ -0,0 +1,9 @@
1
+ # Now
2
+
3
+ Nothing in flight.
4
+
5
+ ## Blocked / uncertain
6
+ None.
7
+
8
+ ## Next
9
+
@@ -0,0 +1,60 @@
1
+ <!-- 由 Keelson 生成;`keelson update` 会刷新。项目事实写入各自负责的文件,不要写进这份地图。 -->
2
+ # Keelson 项目地图 — {{project}}
3
+
4
+ `.keelson/` 保存项目事实、契约、决策与变更证据。只有真正值得保留的信息出现时,它才增长。Agent 指导从已安装的 Keelson 包中读取。
5
+
6
+ ## 从这里开始
7
+
8
+ 人通常只需要看两个文件:
9
+ - **现在在做什么?** → `NOW.md`
10
+ - **项目为什么存在 / 边界是什么?** → `INTENT.md`
11
+
12
+ Agent 从 `keelson guide workflow` 和 `keelson guide` 开始,再按需读取具体 reference。其他内容都按需自动维护;正常开发不需要任何人管理 Keelson 目录。
13
+
14
+ ## init 后始终存在
15
+
16
+ | 路径 | 用途 |
17
+ |---|---|
18
+ | `README.md` | 这份项目地图 |
19
+ | `INTENT.md` | 项目目的、边界、硬约束、Agent 权限 |
20
+ | `NOW.md` | 当前状态、阻塞、下一具体步骤 |
21
+ | `config.yaml` | 用户控制的 Keelson 配置 |
22
+ | `manifest.json` | Keelson 维护的安装清单:它负责哪些宿主生成表面 |
23
+
24
+ ## 只有真正需要时才出现
25
+
26
+ | 路径 | 创建时机 |
27
+ |---|---|
28
+ | `ROADMAP.md` | 项目存在 tracker 没有清楚表达的里程碑/方向 |
29
+ | `GLOSSARY.md` | 共享术语开始重要或出现歧义 |
30
+ | `specs/<capability>/spec.md` | 某能力的可观察行为值得成为契约;契约变大后它自动保持为小型索引 |
31
+ | `specs/<capability>/requirements/*.md` | 某个 capability 契约超出单文件可读范围时自动出现 |
32
+ | `specs/<capability>/decisions/*.md` | capability 局部长期决策增长时自动出现;每个长期决策一个小文件 |
33
+ | `rules/index.md` + `rules/*.md` | 稳定工程不变量适用于某路径,并且不适合直接变成 check |
34
+ | `changes/<name>/change.md` | 非平凡工作需要可评审的边界 |
35
+ | `changes/<name>/tasks.md` | 工作需要明确的多步骤 / 多切片计划 |
36
+ | `changes/<name>/ledger.md` | 真正发生了裁定、根因、分派或验证事件 |
37
+ | `changes/<name>/handoff.md` | 工作所有权真正跨人/跨机器,或需要显式交接包 |
38
+ | `changes/<name>/specs/**` | spec 级变更修改行为契约 |
39
+ | `changes/<name>/decisions.json` | 需要结构化选择及其解决历史时 |
40
+ | `changes/<name>/ledger.jsonl` + `evidence/` | 产生已签名检查记录、签名公钥和按内容寻址的输出时 |
41
+ | `workflow.md` + `skill/` | 负责人明确选择 `--vendor` 复制包内指导时 |
42
+
43
+ 私钥、命令信任、锁和会话焦点保存在本目录之外:Git 私有的 `keelson-runtime` 目录,无 Git 时使用用户缓存。Hook 从已安装的包执行。初始化不会编辑 `.gitignore`;分享检查日志前请审阅其内容。
44
+
45
+ ## 一次变更完成后留下什么
46
+
47
+ 理想结果是**脚手架变少、真相变多**:
48
+
49
+ - 可观察行为 → 主 specs;
50
+ - 稳定约束 → 作用域 rules 或可执行 checks;
51
+ - 稳定术语 → glossary;
52
+ - 普通跨会话接续 → 长期 change 状态 + 本地 session focus;
53
+ - 明确所有权转移 → handoff;
54
+ - 完整时间线 → git 历史。
55
+
56
+ 临时 change 工件在落地后 fold 或 archive。空的可选工件应该删除,而不是为了“以后也许用到”长期保留。
57
+
58
+ ## 人类阅读原则
59
+
60
+ 优先写现在时的当前真相。Keelson 会让高频文件保持有界、自动分片大型 spec,并把内部压缩任务交给 Agent。人不需要执行 housekeeping 命令,也不需要理解底层存储布局才能继续工作。
@@ -0,0 +1,12 @@
1
+ # Roadmap
2
+
3
+ 当前里程碑,以及之后的方向。近期工作写具体;之后的只写方向和依赖。项目有 issue 跟踪系统的话,在这里链接它,把权威清单留在那里。
4
+
5
+ ## Now
6
+ - …(进行中的里程碑,以及"完成"长什么样)
7
+
8
+ ## Next
9
+ - …(方向和已知依赖,不是任务)
10
+
11
+ ## Later
12
+ - …
@@ -0,0 +1,16 @@
1
+ ---
2
+ tier: quick
3
+ created: {{date}}
4
+ status: in-progress
5
+ ---
6
+
7
+ # {{title}}
8
+
9
+ ## Why
10
+
11
+
12
+ ## What
13
+ - …
14
+
15
+ ## Acceptance
16
+ - [ ] … — check: `…`
@@ -0,0 +1,32 @@
1
+ ---
2
+ tier: {{tier}}
3
+ created: {{date}}
4
+ status: clarifying
5
+ ---
6
+
7
+ # {{title}}
8
+
9
+ ## Why
10
+
11
+
12
+ ## What
13
+ - 结果:…
14
+ - 非目标:…
15
+ - …(以加粗的 BREAKING 开头的条目标记破坏性变更,需要一个 Rollout 段)
16
+
17
+ ## How
18
+
19
+
20
+ ## Impact
21
+ - …(调用方、其他入口、数据、权限、兼容性;`keelson impact <files>` 给提示,阅读给答案)
22
+
23
+ ## Acceptance
24
+ - [ ] … — test: `…`
25
+ - [ ] … — manual: …
26
+
27
+ ## Open questions
28
+ - … — blocks: <它阻塞的切片或任务>
29
+
30
+ ## Decisions
31
+ - {{capability}}: …(现在时;只记录未来仍有价值的长期理由)
32
+ - (assumed) {{capability}}: …(你的工作假设;落地前由负责人确认)
@@ -0,0 +1,12 @@
1
+ # {{capability}} — delta
2
+
3
+ ## ADDED Requirements
4
+ ### Requirement: …
5
+ The system SHALL …
6
+ #### Scenario: …
7
+ - WHEN …
8
+ - THEN …
9
+
10
+ ## MODIFIED Requirements
11
+
12
+ ## REMOVED Requirements
@@ -0,0 +1,27 @@
1
+ ---
2
+ at: {{sha}}
3
+ updated: {{date}}
4
+ by: {{by}}
5
+ ---
6
+
7
+ # Handoff — {{name}}
8
+
9
+ 给接手的人或代理看的当前状态摘要。整份覆盖;不要追加日记。
10
+
11
+ ## Goal and confirmed decisions
12
+
13
+
14
+ ## Done
15
+ - …(已完成且已验证的切片或任务)
16
+
17
+ ## Open and blocked
18
+ - …(尚未解决的事,以及它阻塞哪个切片)
19
+
20
+ ## Ruled out
21
+ - …(已否决的假设或方案,附证据)
22
+
23
+ ## Next step
24
+
25
+
26
+ ## Verification
27
+ - last `Verify:` …(命令、退出码、tree);还没检查什么
@@ -0,0 +1,3 @@
1
+ # Ledger — {{name}}
2
+
3
+ 只追加。条目是 `###` 标题:`Ruling:`、`Root cause:`、`Verify:`、`Dispatch:`、`Escalate:`、`Note:`。 `Dispatch:` 正文以 `Result: pass|fail` 开头;`Verify:` 正文写明 `command` 和 `exit N`。
@@ -0,0 +1,7 @@
1
+ <!-- keelson:start -->
2
+ ## Keelson
3
+
4
+ 本项目由 Keelson 管理。非平凡工作开始前运行 `keelson guide` 并按其执行。
5
+ 需要任务专用指导时运行 `keelson guide <reference>`;只有项目需要提交审计副本时才使用 `keelson init --vendor`。
6
+ `.keelson/` 之外的文件只负责宿主发现,不要在这里复制 Keelson 规则。
7
+ <!-- keelson:end -->
@@ -0,0 +1,10 @@
1
+ # General conventions
2
+
3
+ Rules 是前馈控制。每条保持一到两行、范围明确、可检验。描述“什么必须保持成立”,不要把某一种实现配方写死。若某个不变量可以由命令检查,就放进 `config.yaml → check`,正文只保留指针。
4
+
5
+ ## Before editing
6
+ - 与周围风格保持一致;不要重排你本来不需要修改的文件。
7
+ - 无关清理不要塞进当前变更;除非它阻塞需求,否则单独记录、单独处理。
8
+
9
+ ## Checks
10
+ - 编辑过程中尽早运行最便宜且相关的检查;完成或落地前,当前工作树上的 `keelson check --record` 必须通过。
@@ -0,0 +1,5 @@
1
+ # Rules index
2
+
3
+ 每个作用域一行:一个路径 glob、一个箭头、本目录中的 rule 文件。代理只读取 glob 命中它即将改动文件的那些 rule。始终适用的 rule 用 `**`。
4
+
5
+ - `**` → general.md — 适用于所有变更
@@ -0,0 +1,14 @@
1
+ # {{capability}}
2
+
3
+ ## Purpose
4
+ 用一两句话说明这个能力是做什么的。
5
+
6
+ ## Requirement: {{name}}
7
+ The system SHALL …
8
+
9
+ ### Scenario: {{scenario}}
10
+ - WHEN …
11
+ - THEN …
12
+
13
+ ## Decisions
14
+ - {{capability}}: …(每条持久决策一行,现在时,写明被否决的备选)
@@ -0,0 +1,9 @@
1
+ # Tasks
2
+
3
+ 每个任务一个复选框,各带一个 effort 层级,并尽可能带一条验证命令。本文件是可变的执行计划:复选框用于传递进度,但不决定 change 是否已经完成或能否 land。工作有多个可独立交付的部分时分成切片;每个切片写明做完后别人能观察到什么。
4
+
5
+ ## Slice: …
6
+ Delivers: …
7
+
8
+ - [ ] 1. … (effort: light) — verify: `…`
9
+ - [ ] 2. … (effort: standard) — verify: `…`
@@ -0,0 +1,18 @@
1
+ # Keelson 工作流
2
+
3
+ 项目真相与长期 work item 位于 `.keelson/`;本机会话、信任与密钥位于 Git 私有运行目录;持久签名记录和日志跟随变更归档。**Session 不是 Task**:它只是指向当前对话正在围绕哪个 work item。
4
+
5
+ 每个非平凡修改请求遵循 **ORIENT → BOUND → BUILD → SENSE → RECONCILE**。
6
+
7
+ - **ORIENT** —— 检查工作树和当前 session focus。同一目标的追问继续使用 focus change;用户说“继续”时运行 `keelson focus --auto`,存在歧义时绝不静默绑定。
8
+ - **BOUND** —— 提问前先读仓库。仓库事实和可逆工程选择自行解决;只在 decision frontier 每轮询问最多三个独立、已就绪、真正属于所有者且会影响结果的决定。trivial 直接改;quick 创建最小有用 change;spec 写 acceptance、行为 delta 和 plan。只检查当前工作真实触发的风险镜头。
9
+ - **BUILD** —— 一次推进一个纵向切片。独立的新修改目标创建新 change;围绕同一目标继续追问不会。
10
+ - **SENSE** —— 尽早跑便宜检查;完成必须有当前工作树上的新鲜 `keelson check --record` 证据。任务复选框只描述当前计划,不负责判定完成。
11
+ - **RECONCILE** —— 每轮修改后根据 acceptance、阻塞项、rollout/兼容性和新鲜 verification 重新计算生命周期。land 前静默完成 context 暴露的内部知识维护:重写单例当前状态文档、拆分/去重 rules,并让 `land` 自动分片大型 spec。gate 满足后状态成为 `ready` 并自动 land;不等待用户说“做完了”,也不把维护流程暴露给用户。
12
+ - 会话结束、长时间空闲、compaction、关闭窗口只改变本机会话 runtime,绝不自动完成、取消或 land 长期 work item。
13
+ - `handoff.md` 只用于真正跨人/跨机器或明确所有权转移。普通新会话从 change/task/ledger 状态和可用的 session focus 重建。
14
+ - 工件按需创建;空文档不是进度。
15
+ - 重复 failure class 升级为作用域 rule 或可执行 fitness check;自动化接管后删掉冗余 prose。
16
+ - First contact 根据仓库证据推导 `INTENT.md`;只有未决项目边界会实质影响当前工作时才询问所有者。specs/rules 只有真实工作暴露长期真相时才增长。
17
+
18
+ 包内路由器是 `keelson guide`。它判断对话意图;生命周期转换来自 work state,而不是用户措辞。项目仅在使用 `keelson init --vendor` 时才会保存副本。
package/src/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ import { createRequire } from 'node:module';
2
+ import { parseArgs } from './lib/args.js';
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const { version } = require('../package.json');
6
+
7
+ const COMMANDS = {
8
+ ask: ['ask <add|frontier|list|settle|assume|reject|reopen> [id] [--change name] [--json]', 'Persist decisions and show up to three ready owner questions', () => import('./commands/ask.js').then((m) => m.ask)],
9
+ design: ['design [action] [target] [--lang en|zh] [--json]', 'Prepare focused frontend design guidance for your agent', () => import('./commands/design.js').then((m) => m.design)],
10
+ guide: ['guide [reference] [--list] [--json] [--lang en|zh]', 'Read the installed workflow or one reference on demand', () => import('./commands/guide.js').then((m) => m.guide)],
11
+ hook: ['hook <event>', 'Run an installed host adapter', () => import('./commands/hook.js').then((m) => m.hook)],
12
+ attest: ['attest [change] [--json]', 'Export structured evidence and its local trust status', () => import('./commands/attest.js').then((m) => m.attest)],
13
+ init: ['init [--<platform> ...] [--tools a,b] [--guide] [--profile lean|guided] [--lang en|zh] [--no-hooks] [--vendor] [--dry-run]', 'Set up the minimal .keelson/ control plane and host discovery. Project artifacts grow only when the work needs them', () => import('./commands/init.js').then((m) => m.init)],
14
+ platforms: ['platforms [--json]', 'List supported coding tools, their file locations, and which are installed or configured', () => import('./commands/platforms.js').then((m) => m.platforms)],
15
+ update: ['update [--vendor] [--dry-run]', 'Refresh owned host shims and configuration; --vendor opts into copied guidance', () => import('./commands/init.js').then((m) => m.init)],
16
+ context: ['context [--paths a/,b/**] [--json]', 'Print INTENT, ROADMAP, NOW, active changes, existing references, and the rules matching the given paths', () => import('./commands/context.js').then((m) => m.context)],
17
+ impact: ['impact <file> [file...] [--json]', 'Mechanical impact hints: importers, specs and rules that may be affected, active changes that overlap', () => import('./commands/impact.js').then((m) => m.impact)],
18
+ focus: ['focus [change] [--auto|--clear] [--json]', 'Bind this AI session to one active change without changing the change lifecycle', () => import('./commands/focus.js').then((m) => m.focus)],
19
+ new: ['new <name> [--tier quick|spec] [--capability a,b] [--touches globs] [--depends change] [--worktree]', 'Scaffold a change directory (owner, branch, delta base recorded)', () => import('./commands/new.js').then((m) => m.newChange)],
20
+ status: ['status [--json]', 'Work, verification, and release status per change; slices, open questions, conflicts, handoffs', () => import('./commands/status.js').then((m) => m.status)],
21
+ handoff: ['handoff [name] [--by who]', 'Create or re-stamp handoff.md for a change (at, updated, by)', () => import('./commands/handoff.js').then((m) => m.handoff)],
22
+ validate: ['validate [--json]', 'Check .keelson/ structure, specs, changes, ledgers; non-zero on errors', () => import('./commands/validate.js').then((m) => m.validate)],
23
+ check: ['check [cmd...] [--record [claim]] [--change name] [--trust] [--timeout ms] [--quiet] [--json]', 'Run the project checks, save evidence, print or record a Verify entry with the worktree fingerprint', () => import('./commands/check.js').then((m) => m.check)],
24
+ land: ['land [name] [--now "<text>"] [--confirm-assumptions] [--accept-drift] [--keep] [--force --reason "<why>"] [--dry-run]', 'Merge delta specs, fold decisions, remove or archive the change; refuses on stale or missing evidence', () => import('./commands/land.js').then((m) => m.land)],
25
+ cancel: ['cancel <name> [--reason "<why>"]', 'Archive a change as cancelled without merging anything', () => import('./commands/land.js').then((m) => m.cancel)],
26
+ retro: ['retro [--json]', 'Metrics from ledgers plus suggestions to prune guidance or add rules', () => import('./commands/retro.js').then((m) => m.retro)],
27
+ models: ['models [--detect] [--refresh] [--resolve <tier>] [rank <alias> <tier>] [--platform <id>]', 'Resolve effort tiers to model aliases for this platform', () => import('./commands/models.js').then((m) => m.models)],
28
+ doctor: ['doctor [--session] [--json]', 'Diagnose the install: versions, hooks, config migration, validation, stale evidence, conflicts', () => import('./commands/doctor.js').then((m) => m.doctor)],
29
+ ablate: ['ablate [--dry-run]', 'Temporarily disable Keelson integration, preserving it for restore', () => import('./commands/ablate.js').then((m) => m.ablate)],
30
+ restore: ['restore [--force] [--dry-run]', 'Restore an ablated project byte-for-byte', () => import('./commands/ablate.js').then((m) => m.restore)],
31
+ uninstall: ['uninstall [--purge]', 'Remove generated surfaces; keep .keelson/ unless --purge', () => import('./commands/uninstall.js').then((m) => m.uninstall)],
32
+ };
33
+
34
+ const COMMAND_GROUPS = [
35
+ ['Your commands', ['init', 'design', 'status', 'doctor', 'update', 'platforms', 'uninstall']],
36
+ ['Agent workflow', ['ask', 'context', 'impact', 'focus', 'new', 'check', 'handoff', 'validate', 'land', 'cancel']],
37
+ ['Maintenance / advanced', ['guide', 'attest', 'retro', 'models', 'ablate', 'restore']],
38
+ ];
39
+
40
+ export function help({ all = false } = {}) {
41
+ const lines = [
42
+ `keelson ${version} — verifiable checks and durable decisions for coding agents`,
43
+ '',
44
+ 'Usage: keelson <command> [options]',
45
+ 'Normal use: run `keelson init` once, then talk to your coding agent as usual.',
46
+ ];
47
+ for (const [title, names] of COMMAND_GROUPS) {
48
+ lines.push('', `${title}:`);
49
+ for (const name of names) {
50
+ const [usage, desc] = COMMANDS[name];
51
+ if (all) lines.push(` ${usage}`, ` ${desc}`);
52
+ else lines.push(` ${name.padEnd(12)} ${desc.split(';')[0].split('. ')[0]}`);
53
+ }
54
+ }
55
+ lines.push('', 'Details: keelson help <command> · All options: keelson --help --all', '', 'Docs: https://github.com/Atingaii/keelson/tree/main/docs');
56
+ return lines.join('\n');
57
+ }
58
+
59
+ export async function main(argv) {
60
+ const { flags, positional } = parseArgs(argv);
61
+ let cmd = positional.shift();
62
+ if (flags.h) flags.help = true;
63
+ if (flags.v) flags.version = true;
64
+ if (cmd === 'help') {
65
+ cmd = positional.shift();
66
+ flags.help = true;
67
+ }
68
+ if (flags.version || cmd === 'version') {
69
+ console.log(version);
70
+ return 0;
71
+ }
72
+ if (flags.help && cmd && Object.hasOwn(COMMANDS, cmd)) {
73
+ console.log(`Usage: keelson ${COMMANDS[cmd][0]}\n\n ${COMMANDS[cmd][1]}`);
74
+ return 0;
75
+ }
76
+ if (!cmd) {
77
+ console.log(help({ all: Boolean(flags.all) }));
78
+ return 0;
79
+ }
80
+ const entry = Object.hasOwn(COMMANDS, cmd) ? COMMANDS[cmd] : undefined;
81
+ if (!entry) {
82
+ console.error(`unknown command "${cmd}". Run \`keelson --help\` to list commands.`);
83
+ return 2;
84
+ }
85
+ const run = await entry[2]();
86
+ return run({ flags, positional });
87
+ }
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import { requireProjectRoot, projectPaths, USER_HOME } from '../lib/paths.js';
5
+ import { exists, isDir, copyDir, rmrf, readJson, writeJson, mkdirp, walk, read } from '../lib/fs.js';
6
+ import { loadConfig } from '../lib/config.js';
7
+ import { removeSurfaces, PLATFORMS, installTargets, managedTargets } from '../platforms/index.js';
8
+ import { ok, warn, heading, info } from '../lib/out.js';
9
+
10
+ const stashDir = (root) => path.join(USER_HOME, 'ablations', crypto.createHash('sha1').update(root).digest('hex').slice(0, 12));
11
+
12
+ const hashTree = (dir) => {
13
+ const h = crypto.createHash('sha256');
14
+ for (const f of walk(dir)) h.update(f).update('\0').update(read(path.join(dir, f))).update('\0');
15
+ return h.digest('hex');
16
+ };
17
+ const hashPath = (p) => (exists(p) ? (isDir(p) ? 'dir:' + hashTree(p) : 'file:' + crypto.createHash('sha256').update(read(p)).digest('hex')) : null);
18
+
19
+ export async function ablate({ flags }, cwd = process.cwd()) {
20
+ const root = requireProjectRoot(cwd);
21
+ const p = projectPaths(root);
22
+ const cfg = loadConfig(p.config);
23
+ const stash = stashDir(root);
24
+ if (exists(path.join(stash, 'manifest.json'))) throw new Error(`an ablation for this project already exists (${stash}); run \`keelson restore\` first`);
25
+ heading(`Ablate Keelson from ${root}`);
26
+ const surfaces = [];
27
+ const recordedTargets = managedTargets(root);
28
+ const surfaceTargets = recordedTargets.length ? recordedTargets : installTargets((cfg.tools ?? []).filter((t) => PLATFORMS[t]), cfg);
29
+ for (const pl of surfaceTargets) {
30
+ surfaces.push(pl.instructions, path.join(pl.skillsDir, 'keelson'));
31
+ if (pl.rulesFile) surfaces.push(pl.rulesFile);
32
+ if (pl.hooks) surfaces.push('.claude/settings.json');
33
+ if (pl.sessionAdapter === 'opencode-plugin') surfaces.push('.opencode/plugins/keelson-session.js');
34
+ if (pl.sessionAdapter === 'codebuddy-hooks') surfaces.push('.codebuddy/settings.json');
35
+ }
36
+ // Canonical runtime, hook scripts, session runtime and project facts are
37
+ // stashed as one directory so ablate/restore is byte-for-byte transactional.
38
+ surfaces.push('.keelson');
39
+ if (flags.dryRun) {
40
+ for (const s of surfaces) if (exists(path.join(root, s))) info(`would stash ${s}`);
41
+ return 0;
42
+ }
43
+ mkdirp(stash);
44
+ // Project notes and host configuration can contain sensitive data. Restrict
45
+ // the recovery directory before copying, including a reused empty directory.
46
+ if (process.platform !== 'win32') fs.chmodSync(stash, 0o700);
47
+ const manifest = { root, created: new Date().toISOString(), files: [] };
48
+ for (const s of [...new Set(surfaces)]) {
49
+ const src = path.join(root, s);
50
+ if (!exists(src)) continue;
51
+ const dest = path.join(stash, 'files', s);
52
+ mkdirp(path.dirname(dest));
53
+ copyDir(src, dest);
54
+ manifest.files.push(s);
55
+ }
56
+ manifest.hash = hashTree(path.join(stash, 'files'));
57
+ removeSurfaces(root, cfg.tools ?? [], cfg);
58
+ rmrf(p.keelson);
59
+ // What each managed path looks like right after ablation (null = removed). Restore compares against this.
60
+ manifest.after = Object.fromEntries(manifest.files.map((s) => [s, hashPath(path.join(root, s))]));
61
+ writeJson(path.join(stash, 'manifest.json'), manifest);
62
+ for (const s of manifest.files) ok(`stashed ${s}`);
63
+ info(`recovery transaction: ${stash}`);
64
+ warn('start a fresh agent session for the comparison; `keelson restore` brings everything back byte-for-byte');
65
+ return 0;
66
+ }
67
+
68
+ export async function restore({ flags }, cwd = process.cwd()) {
69
+ const root = path.resolve(flags.dir ?? cwd);
70
+ const stash = stashDir(root);
71
+ const manifest = readJson(path.join(stash, 'manifest.json'));
72
+ if (!manifest) throw new Error(`no ablation found for ${root}`);
73
+ if (hashTree(path.join(stash, 'files')) !== manifest.hash) throw new Error('stash contents changed since ablation; refusing to restore');
74
+ heading(`Restore Keelson to ${root}`);
75
+ for (const s of manifest.files) {
76
+ const dest = path.join(root, s);
77
+ const now = hashPath(dest);
78
+ const expected = manifest.after?.[s] ?? null;
79
+ if (now !== expected && !flags.force) {
80
+ throw new Error(`${s} changed while ablated; refusing to overwrite it. Resolve it by hand or pass --force.`);
81
+ }
82
+ }
83
+ if (flags.dryRun) {
84
+ for (const s of manifest.files) info(`would restore ${s}`);
85
+ return 0;
86
+ }
87
+ for (const s of manifest.files) {
88
+ const dest = path.join(root, s);
89
+ rmrf(dest);
90
+ mkdirp(path.dirname(dest));
91
+ copyDir(path.join(stash, 'files', s), dest);
92
+ ok(`restored ${s}`);
93
+ }
94
+ rmrf(stash);
95
+ return 0;
96
+ }
@@ -0,0 +1,64 @@
1
+ import { requireProjectRoot, projectPaths } from '../lib/paths.js';
2
+ import { loadAllChanges } from '../lib/changes.js';
3
+ import { readSession } from '../lib/session.js';
4
+ import { readDecisions, updateDecisions, decisionFrontier } from '../lib/decisions.js';
5
+ import path from 'node:path';
6
+ import { withLock } from '../lib/fs.js';
7
+ import { runtimeDir } from '../lib/runtime-path.js';
8
+
9
+ export async function ask(args, cwd = process.cwd()) {
10
+ const root = requireProjectRoot(cwd);
11
+ return withLock(path.join(runtimeDir(root), 'landing'), () => askUnlocked(args, cwd));
12
+ }
13
+
14
+ function askUnlocked({ positional, flags }, cwd) {
15
+ const root = requireProjectRoot(cwd);
16
+ const changes = loadAllChanges(projectPaths(root).changes);
17
+ const name = flags.change ?? readSession(root).state?.change ?? (changes.length === 1 ? changes[0].name : null);
18
+ const change = changes.find((c) => c.name === name);
19
+ if (!change) throw new Error('choose an active change with --change <name>');
20
+ const [action = 'frontier', id] = positional;
21
+ let data = readDecisions(change.dir);
22
+ const text = (value, label) => {
23
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${label} is required`);
24
+ return value.trim();
25
+ };
26
+ if (action === 'add') {
27
+ if (!id || !/^[\p{L}\p{N}_-]+$/u.test(id)) throw new Error('decision ID must be a nonempty word');
28
+ const owner = flags.owner ?? 'user';
29
+ if (!['user', 'agent', 'reality'].includes(owner)) throw new Error('--owner must be user, agent or reality');
30
+ const question = text(flags.question, '--question');
31
+ data = updateDecisions(change.dir, (doc) => {
32
+ if (doc.decisions.some((d) => d.id === id || d.question.toLowerCase() === question.toLowerCase())) throw new Error('decision already recorded; reuse its ID');
33
+ doc.decisions.push({ id, question, owner, state: 'open', depends: flags.depends ? String(flags.depends).split(',').map((v) => v.trim()).filter(Boolean) : [], irreversible: Boolean(flags.irreversible), recommended: typeof flags.recommend === 'string' ? flags.recommend : null, history: [] });
34
+ });
35
+ } else if (['settle', 'assume', 'reject', 'reopen'].includes(action)) {
36
+ data = updateDecisions(change.dir, (doc) => {
37
+ const d = doc.decisions.find((item) => item.id === id);
38
+ if (!d) throw new Error(`unknown decision ${id}`);
39
+ if (action === 'reopen') {
40
+ const reason = text(flags.reason, '--reason');
41
+ if (d.state === 'open') throw new Error(`${id} is already open`);
42
+ d.history.push({ at: new Date().toISOString(), state: d.state, answer: d.answer ?? null, basis: d.basis ?? null, reason });
43
+ d.state = 'open'; delete d.answer; delete d.basis;
44
+ } else {
45
+ if (d.state !== 'open') throw new Error(`${id} is ${d.state}; use reopen --reason before changing a settled answer`);
46
+ if (!d.depends.every((dep) => doc.decisions.some((x) => x.id === dep && x.state === 'settled'))) throw new Error(`${id} has unresolved dependencies`);
47
+ if (action === 'assume' && d.irreversible) throw new Error('irreversible decisions require settlement, not an assumption');
48
+ d.basis = text(flags.basis ?? flags.reason, '--basis');
49
+ d.answer = action === 'reject' ? null : text(flags.answer, '--answer');
50
+ d.state = { settle: 'settled', assume: 'assumed', reject: 'rejected' }[action];
51
+ d.history.push({ at: new Date().toISOString(), state: d.state, answer: d.answer, basis: d.basis });
52
+ }
53
+ });
54
+ } else if (!['frontier', 'list'].includes(action)) throw new Error('usage: keelson ask <add|frontier|list|settle|assume|reject|reopen> [id] --change <name>');
55
+ const result = action === 'list' ? data : decisionFrontier(data);
56
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
57
+ else if (action === 'list') for (const d of data.decisions) console.log(`${d.id} [${d.owner}/${d.state}] ${d.question}${d.answer ? ` → ${d.answer}` : ''}`);
58
+ else {
59
+ for (const d of result.questions) console.log(`${d.id}. ${d.question}${d.recommended ? ` (recommended: ${d.recommended})` : ''}`);
60
+ for (const d of result.investigate) console.log(`Investigate ${d.id} (${d.owner}): ${d.question}`);
61
+ if (!result.questions.length) console.log(result.complete ? 'Decision frontier complete.' : 'No owner question is ready; resolve investigations, dependencies or assumptions.');
62
+ }
63
+ return 0;
64
+ }