@shgroup/opencode-serenity-plugin 0.2.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 (151) hide show
  1. package/README.md +199 -0
  2. package/bin/opencode-serenity-plugin.js +316 -0
  3. package/dist/activation.d.ts +40 -0
  4. package/dist/activation.d.ts.map +1 -0
  5. package/dist/activation.js +133 -0
  6. package/dist/activation.js.map +1 -0
  7. package/dist/bash-override.d.ts +10 -0
  8. package/dist/bash-override.d.ts.map +1 -0
  9. package/dist/bash-override.js +24 -0
  10. package/dist/bash-override.js.map +1 -0
  11. package/dist/bash-toggle.d.ts +17 -0
  12. package/dist/bash-toggle.d.ts.map +1 -0
  13. package/dist/bash-toggle.js +42 -0
  14. package/dist/bash-toggle.js.map +1 -0
  15. package/dist/config-schema.d.ts +300 -0
  16. package/dist/config-schema.d.ts.map +1 -0
  17. package/dist/config-schema.js +185 -0
  18. package/dist/config-schema.js.map +1 -0
  19. package/dist/errors.d.ts +90 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +151 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/fs/file-system-tool.d.ts +25 -0
  24. package/dist/fs/file-system-tool.d.ts.map +1 -0
  25. package/dist/fs/file-system-tool.js +318 -0
  26. package/dist/fs/file-system-tool.js.map +1 -0
  27. package/dist/fs/resolve-path.d.ts +53 -0
  28. package/dist/fs/resolve-path.d.ts.map +1 -0
  29. package/dist/fs/resolve-path.js +100 -0
  30. package/dist/fs/resolve-path.js.map +1 -0
  31. package/dist/hooks/compacting.d.ts +23 -0
  32. package/dist/hooks/compacting.d.ts.map +1 -0
  33. package/dist/hooks/compacting.js +90 -0
  34. package/dist/hooks/compacting.js.map +1 -0
  35. package/dist/hooks/permission-auto-reply.d.ts +91 -0
  36. package/dist/hooks/permission-auto-reply.d.ts.map +1 -0
  37. package/dist/hooks/permission-auto-reply.js +158 -0
  38. package/dist/hooks/permission-auto-reply.js.map +1 -0
  39. package/dist/hooks/permission-guards.d.ts +41 -0
  40. package/dist/hooks/permission-guards.d.ts.map +1 -0
  41. package/dist/hooks/permission-guards.js +153 -0
  42. package/dist/hooks/permission-guards.js.map +1 -0
  43. package/dist/hooks/shell-env.d.ts +20 -0
  44. package/dist/hooks/shell-env.d.ts.map +1 -0
  45. package/dist/hooks/shell-env.js +38 -0
  46. package/dist/hooks/shell-env.js.map +1 -0
  47. package/dist/hooks/util.d.ts +81 -0
  48. package/dist/hooks/util.d.ts.map +1 -0
  49. package/dist/hooks/util.js +172 -0
  50. package/dist/hooks/util.js.map +1 -0
  51. package/dist/index.d.ts +26 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +63 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/init/init-wizard.d.ts +39 -0
  56. package/dist/init/init-wizard.d.ts.map +1 -0
  57. package/dist/init/init-wizard.js +297 -0
  58. package/dist/init/init-wizard.js.map +1 -0
  59. package/dist/install.d.ts +117 -0
  60. package/dist/install.d.ts.map +1 -0
  61. package/dist/install.js +255 -0
  62. package/dist/install.js.map +1 -0
  63. package/dist/msm-schema.d.ts +76 -0
  64. package/dist/msm-schema.d.ts.map +1 -0
  65. package/dist/msm-schema.js +207 -0
  66. package/dist/msm-schema.js.map +1 -0
  67. package/dist/msm.d.ts +25 -0
  68. package/dist/msm.d.ts.map +1 -0
  69. package/dist/msm.js +317 -0
  70. package/dist/msm.js.map +1 -0
  71. package/dist/session/lib.d.ts +33 -0
  72. package/dist/session/lib.d.ts.map +1 -0
  73. package/dist/session/lib.js +475 -0
  74. package/dist/session/lib.js.map +1 -0
  75. package/dist/session/session-tool.d.ts +17 -0
  76. package/dist/session/session-tool.d.ts.map +1 -0
  77. package/dist/session/session-tool.js +109 -0
  78. package/dist/session/session-tool.js.map +1 -0
  79. package/dist/skills/install-skill.d.ts +36 -0
  80. package/dist/skills/install-skill.d.ts.map +1 -0
  81. package/dist/skills/install-skill.js +91 -0
  82. package/dist/skills/install-skill.js.map +1 -0
  83. package/dist/skills/template-loader.d.ts +79 -0
  84. package/dist/skills/template-loader.d.ts.map +1 -0
  85. package/dist/skills/template-loader.js +170 -0
  86. package/dist/skills/template-loader.js.map +1 -0
  87. package/dist/state.d.ts +35 -0
  88. package/dist/state.d.ts.map +1 -0
  89. package/dist/state.js +62 -0
  90. package/dist/state.js.map +1 -0
  91. package/dist/tui.d.ts +61 -0
  92. package/dist/tui.d.ts.map +1 -0
  93. package/dist/tui.js +279 -0
  94. package/dist/tui.js.map +1 -0
  95. package/dist/types/index.d.ts +30 -0
  96. package/dist/types/index.d.ts.map +1 -0
  97. package/dist/types/index.js +17 -0
  98. package/dist/types/index.js.map +1 -0
  99. package/dist/util/config-patch.d.ts +58 -0
  100. package/dist/util/config-patch.d.ts.map +1 -0
  101. package/dist/util/config-patch.js +117 -0
  102. package/dist/util/config-patch.js.map +1 -0
  103. package/dist/util/git.d.ts +29 -0
  104. package/dist/util/git.d.ts.map +1 -0
  105. package/dist/util/git.js +74 -0
  106. package/dist/util/git.js.map +1 -0
  107. package/dist/util/init-check.d.ts +22 -0
  108. package/dist/util/init-check.d.ts.map +1 -0
  109. package/dist/util/init-check.js +76 -0
  110. package/dist/util/init-check.js.map +1 -0
  111. package/dist/util/init.d.ts +54 -0
  112. package/dist/util/init.d.ts.map +1 -0
  113. package/dist/util/init.js +87 -0
  114. package/dist/util/init.js.map +1 -0
  115. package/dist/util/log.d.ts +25 -0
  116. package/dist/util/log.d.ts.map +1 -0
  117. package/dist/util/log.js +28 -0
  118. package/dist/util/log.js.map +1 -0
  119. package/dist/util/msm-call.d.ts +48 -0
  120. package/dist/util/msm-call.d.ts.map +1 -0
  121. package/dist/util/msm-call.js +86 -0
  122. package/dist/util/msm-call.js.map +1 -0
  123. package/dist/util/msm-exec-runtime.d.ts +123 -0
  124. package/dist/util/msm-exec-runtime.d.ts.map +1 -0
  125. package/dist/util/msm-exec-runtime.js +532 -0
  126. package/dist/util/msm-exec-runtime.js.map +1 -0
  127. package/dist/util/path.d.ts +10 -0
  128. package/dist/util/path.d.ts.map +1 -0
  129. package/dist/util/path.js +21 -0
  130. package/dist/util/path.js.map +1 -0
  131. package/dist/util/ready-state.d.ts +43 -0
  132. package/dist/util/ready-state.d.ts.map +1 -0
  133. package/dist/util/ready-state.js +104 -0
  134. package/dist/util/ready-state.js.map +1 -0
  135. package/dist/util/serenity-file.d.ts +30 -0
  136. package/dist/util/serenity-file.d.ts.map +1 -0
  137. package/dist/util/serenity-file.js +69 -0
  138. package/dist/util/serenity-file.js.map +1 -0
  139. package/dist/util/tui-install.d.ts +61 -0
  140. package/dist/util/tui-install.d.ts.map +1 -0
  141. package/dist/util/tui-install.js +94 -0
  142. package/dist/util/tui-install.js.map +1 -0
  143. package/docs/architecture-v0.md +294 -0
  144. package/docs/contract-v0.md +417 -0
  145. package/docs/plugin-self-contained-msm-v1.md +182 -0
  146. package/docs/refactor-direction-v1.11.md +78 -0
  147. package/docs/requirements-v0-scope.md +104 -0
  148. package/docs/requirements-v0-summary.md +108 -0
  149. package/docs/rr7-init-design.md +304 -0
  150. package/docs/v0.1-candidates.md +132 -0
  151. package/package.json +54 -0
@@ -0,0 +1,104 @@
1
+ # Requirements v0 — Scope(范围层)
2
+
3
+ > **plugin**: opencode-serenity-plugin
4
+ > **版本**: v0 scope
5
+ > **日期**: 2026-06-04
6
+ > **状态**: ✅ 范围层关闭(RR1-RR7 + RR7 5 子点全部已定;② 命名模型在 v1.10 精修为 prefix + `-serenity` 后缀)
7
+ > **下一抽象层**: 方案/设计层(plugin 启动协议 + 整体架构;RR7 详细设计见 `docs/rr7-init-design.md`)
8
+
9
+ ---
10
+
11
+ ## 1 顶层目的
12
+
13
+ > **将 opencode 变成 serenity 专属 agent。**
14
+
15
+ plugin 工作后:
16
+ - opencode 的默认通用能力被**强约束**(禁 bash、目录外无权限、强制 skill 加载)
17
+ - opencode 看到的是 **serenity 工作流**(MSM 工具集、SSO 凭证、领域 SKILL.md)
18
+ - 非 serenity 目录 = plugin **完全不工作**("就像没装一样")
19
+
20
+ ---
21
+
22
+ ## 2 RR1-RR7 完整规则(7 条产品行为约束)
23
+
24
+ | # | 规则 | 关键约束 |
25
+ |---|------|---------|
26
+ | **RR1** | cwd 内必须有 `/.serenity` 文件 | 文件内容 = 实例名 N(字符串)|
27
+ | **RR2** | plugin 激活后**首次**加载对应实例的 SKILL.md | 路径:`.opencode/skills/<N>/SKILL.md`;每次新 session 启动时加载 |
28
+ | **RR3** | **禁 bash** 工具 | 所有命令必须通过 MSM(已有或现场编写后注册)|
29
+ | **RR4** | cwd 内全部权限 | read/write/edit/delete/execute 默认 allow |
30
+ | **RR5** | cwd 外全部无权限 | deny / throw / no-op(具体形式待方案层定)|
31
+ | **RR6** | cwd 必须在 git repo 内 | 满足 `git rev-parse --is-inside-work-tree`;否则 plugin 不工作 |
32
+ | **RR7** | plugin 应能"将 cwd 初始化为 serenity 目录" | 详见 §3(5 子点)+ `docs/rr7-init-design.md`(v1.10 实施设计)|
33
+
34
+ **统一不工作规则**:`/.serenity` 不存在 **或** 不在 git repo **或** `.opencode/skills/<N>/SKILL.md` 找不到 → plugin **完全不工作**(就像没装一样,无任何副作用)。
35
+
36
+ ---
37
+
38
+ ## 3 RR7 5 子点(产品行为细节)
39
+
40
+ | # | 决策 | 说明 |
41
+ |---|------|------|
42
+ | ① 触发方式 | slash command **`/serenity-init`** | 用户在 opencode TUI 中执行 |
43
+ | ② 实例命名模型 | **prefix + `-serenity` 后缀** | 用户输入 prefix(kebab-case);最终实例名 = `${prefix}-serenity`;例:`xx` → `xx-serenity`、`tg` → `tg-serenity`;默认 prefix = 目录名(kebab-case 转换;带 `-serenity` 后缀时自动剥除)|
44
+ | ③ git 前置 | **不自动** init | 要求用户先自己 `git init`;plugin 失败时给出明确提示 |
45
+ | ④ commit 行为 | 创建后**自动** `git add + commit` | commit message 模板:`chore: initialize serenity (instance: <N>)` |
46
+ | ⑤ 初始化范围 | **仅**创建 `/.serenity` | 最小化原则;不创建 `.opencode/skills/<N>/`(用户自行准备)|
47
+
48
+ **slash command 失败处理**(详见 `docs/rr7-init-design.md` §5 失败矩阵):
49
+ - 不在 git repo 内 → toast 提示"请先 `git init`"
50
+ - prefix 不合法(非 kebab-case)→ toast 提示规则,**dialog 保持开启**让用户重试
51
+ - `/.serenity` 已存在 → toast 提示"已是 serenity 目录 (instance: N)",**no-op**(不主动更新实例名)
52
+ - git add/commit 失败 → 回滚 `/.serenity`,toast 提示错误详情
53
+ - 验证失败 vs 软失败:验证失败抛错(dialog 保持),软失败(已是)返回值(dialog 关闭)
54
+
55
+ ---
56
+
57
+ ## 4 抽象层位置(5 层链)
58
+
59
+ ```
60
+ 需求/目标层 → "将 opencode 变成 serenity 专属 agent" ← 顶层目的(§1)
61
+ 范围/边界层 → RR1-RR7 ← 本文档
62
+ 方案/设计层 → Q4 plugin 启动协议、整体架构 ← 下一层(待讨论)
63
+ 接口/契约层 → msm_exec 签名、permission schema、SKILL.md 接口 ← 方案层后再谈
64
+ 实现/代码层 → src/plugin.ts、TS 类型、单元测试 ← 最后
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 5 范围层外不讨论(撤回记录)
70
+
71
+ 以下问题在范围层讨论时被**撤回**,移到对应抽象层:
72
+
73
+ | 撤回项 | 原问方式 | 撤回原因 | 移到哪层 |
74
+ |--------|---------|---------|---------|
75
+ | msm_exec 签名 | `Q5` | 这是接口/契约层细节,需求层不该问 | 接口/契约层 |
76
+ | permission schema | `Q6` | 同上 | 接口/契约层 |
77
+ | plugin 仓"身份" | `Q8` | 废问题——plugin 仓就是 plugin 仓,无身份问题 | 删除 |
78
+ | `HOME_SERENITY_ROOT` env | `Q7-选项` | 已被"cwd 就是主仓"取代 | 删除 |
79
+
80
+ ---
81
+
82
+ ## 6 关联文档
83
+
84
+ | 文档 | 路径 | 关系 |
85
+ |------|------|------|
86
+ | plugin 仓 README | `README.md` | 总入口;引用本文档 |
87
+ | plugin 仓 SESSION | `SESSION.md` | 活跃跟进载体;记录所有变更历史 |
88
+ | **旧版需求**(R1-R5,已被 RR1-RR7 取代)| `docs/requirements-v0-summary.md` | 4.8KB 摘要;**已过时**,保留作为演进历史 |
89
+ | 主仓调研 SESSION | `../HOME-SERENITY/AGENT_SESSIONS/2026-06-04--opencode-plugin-investigation/` | 软引用(D11 决策);含源码分析、扩展点定位 |
90
+ | 主仓 v0 锁定(中间态)| 调研 SESSION 内的 `requirements-locked-v0.md` | R1-R5 临时状态;本 RR1-RR7 是其正式演化结果 |
91
+
92
+ ---
93
+
94
+ ## 7 未决 / 待进入下一层
95
+
96
+ - 方案/设计层启动时机
97
+ - 方案层第一个讨论议题(推荐:Q4 plugin 启动协议)
98
+ - msm_exec 签名(接口/契约层)
99
+ - permission schema(接口/契约层)
100
+ - plugin 仓 README 是否要回写"产品需求"摘要链接
101
+
102
+ ---
103
+
104
+ > **本文档是范围/边界层的最终收口。** 任何对 RR1-RR7 的修订都应改本文档 + 在 plugin 仓 SESSION.md 留 git 历史。
@@ -0,0 +1,108 @@
1
+ # v0 需求摘要(5 条 R1-R5)⚠️ 已过时
2
+
3
+ > **源文档**:`AGENT_SESSIONS/2026-06-04--opencode-plugin-investigation/requirements-locked-v0.md`
4
+ > **本文件作用**:在 plugin 仓内快速引用,避免每次回主仓查。
5
+ > **同步约定**:调研 SESSION 文档若改动,本文件手动更新(**不**自动同步——需求一旦锁定即冻结)。
6
+ >
7
+ > ⚠️ **已过时**(v0.0.2 — 2026-06-07):R1-R5 已被 RR1-RR7 完全取代,本文件仅保留演进历史。**新需求请阅 [`docs/requirements-v0-scope.md`](./requirements-v0-scope.md)**。
8
+
9
+ ---
10
+
11
+ ## R1:bash 替换为 msm 执行工具
12
+
13
+ **问题**:LLM 可用 `bash { command: "ssh root@ip ..." }` 静默绕开 ssh-connect
14
+
15
+ **方案**(双层):
16
+ 1. **plugin 层**:在 `package.json#opencode.tools` 注册同名 `bash` tool,**每次调用直接 throw 错误**("此环境禁用 bash,请使用 msm_exec")
17
+ 2. **opencode.json 层**:在 `permission.bash` 设 `"deny"`
18
+
19
+ **验收 F1**:plugin 加载后,LLM 工具列表中 `bash` 仍可见(同名覆盖),但调用必抛错。
20
+ **验收 F2**:即使绕过 plugin(直接改 opencode.json),`permission.bash:"deny"` 也兜底。
21
+
22
+ ---
23
+
24
+ ## R2:1+1 msm 设计
25
+
26
+ **问题**:原"31 tool 化"会让 LLM 注意力分散(L4 §7.2)
27
+
28
+ **方案**:**只注册 2 个 tool**:
29
+ - `msm_list`:返回 31 条 MSM 清单(按 category 分组),描述每个 MSM 用途
30
+ - `msm_exec(msm_name, args)`:执行指定 MSM
31
+
32
+ **验收 F3**:`msm_list` 返回结构化清单,category 至少含:`ssh-connect / resolve-path / mech-manifest / server-health-check / orbit-status / container-mgmt / vllm-status / health-check / session-qa / create-session / archive / eap-diagnose / eap-checklists / check-triggers / repo-scan / scan-network / batch-sync / batch-clone / ssh-host / ...`
33
+ **验收 F4**:`msm_exec` 调用 `ssh-connect --host <name> --exec "<cmd>"` 时透传参数。
34
+ **验收 F5**:任意非 serenity 目录下 `msm_list` 仍能工作(R5 单独处理 exec)。
35
+
36
+ ---
37
+
38
+ ## R3:read 弹窗静态白名单
39
+
40
+ **问题**:opencode.json 当前 `permission` 只声明 `task`,read 每次弹 ask
41
+
42
+ **方案**:**opencode.json 静态配** `permission.read: "allow"`(**不开 plugin hook**——纯配置够用)
43
+
44
+ **验收 F6**:read 操作不弹窗。
45
+ **验收 F7**:edit / webfetch 仍弹(v0 不可行项,v1 PoC 再处理)。
46
+
47
+ ---
48
+
49
+ ## R4:home-serenity primary-agent 集成
50
+
51
+ **问题**:
52
+ - 当前 `default_agent: "home-serenity"` 在 agent 字典未定义 → `throw new Error`(L3 验证 agent.ts:315)
53
+ - `cheap-worker` 仍 enabled
54
+
55
+ **方案**(主仓 opencode.json 改动,**不在本仓**):
56
+ 1. 在 `agent` 字典定义 `home-serenity` 主 agent(prompt 引用 system prompt + 必加载 skill 列表)
57
+ 2. 设 `cheap-worker.enabled: false`
58
+ 3. 保留 `build` / `plan` 为 subagent(`enabled: false` 保留为将来 v1 启用)
59
+
60
+ **验收 F8**:启动不再 throw。
61
+ **验收 F9**:主 agent 名称 "home-serenity" 出现在 LLM system prompt 顶部。
62
+ **验收 F10**:cheap-worker 不可见 / 不可用。
63
+
64
+ ---
65
+
66
+ ## R5:作用域门控(degraded mode + env var)
67
+
68
+ **问题**:plugin 应只在宁静号目录工作,防止误用到其他项目
69
+
70
+ **方案**(DR7-DR9):
71
+ - **DR7**:非 serenity 目录走 **degraded mode**(不静默 no-op)
72
+ - `msm_list` 返回 `[{"name": "info", "description": "本环境已禁用 msm 工具(HOME_SERENITY_RESTRICT=true)", "category": "system"}]`
73
+ - `msm_exec` 抛错 `"msm_exec unavailable: HOME_SERENITY_RESTRICT=true, 请在宁静号根目录运行"`
74
+ - **DR8**:判定 = `process.cwd()` 是否以 `$HOME_SERENITY_ROOT` 为前缀
75
+ - **DR9**:行为受 `HOME_SERENITY_RESTRICT` env var 控制(默认 `true`)
76
+
77
+ **验收 S1**:在 serenity 目录下 `msm_list` 返回完整 31 条。
78
+ **验收 S2**:在 `/tmp` 下 `msm_list` 返回 degraded 单条说明。
79
+ **验收 S3**:在 `/tmp` 下 `msm_exec("ssh-connect", ...)` 抛错。
80
+ **验收 S4**:`HOME_SERENITY_RESTRICT=false` 时所有目录都正常(escape hatch)。
81
+
82
+ ---
83
+
84
+ ## 不可行项(v0 明确不做)
85
+
86
+ | # | 项 | 原因 | 后续 |
87
+ |---|----|------|------|
88
+ | I1 | 100% 无弹窗(edit / webfetch 仍弹)| `permission.ask` hook 是死声明(L3 验证)| v1 PoC 用 event hook + SDK reply |
89
+ | I2 | 0 维护成本 | plugin 必须跟随 opencode 升级 | 锁 commit + 每次 release 回归 |
90
+ | I3 | 0 延迟 | 物理规律 | 接受 |
91
+ | I4 | 纯 prompt 替代 plugin | L1 证伪 8 处软约束失效 | 不可能 |
92
+
93
+ ---
94
+
95
+ ## 20 条验收索引
96
+
97
+ - **F1-F10**:功能性(10 条)
98
+ - **P1-P2**:性能/兼容性(2 条)
99
+ - **S1-S4**:作用域/隔离(4 条)
100
+ - **M1-M3**:可维护性(3 条)
101
+ - **总计 19 条**(源文档是 20 条 = F1-F14 + P1-P2 + S1-S4 + M1-M3;本摘要仅列 10 条关键 F)
102
+
103
+ **完整验收**见源文档 §3。
104
+
105
+ ---
106
+
107
+ > **本文件锁定日期**:2026-06-04
108
+ > **如需求改动**:回主仓 SESSION 改 `requirements-locked-v0.md`,**不**改本文件(防止两处不同步)
@@ -0,0 +1,304 @@
1
+ # RR7 Init — Design Doc
2
+
3
+ > **plugin**: opencode-serenity-plugin
4
+ > **版本**: v1.10 (RR7 实现)
5
+ > **日期**: 2026-06-06
6
+ > **状态**: 📐 设计层(待 review → 实施)
7
+ > **上游**: `docs/requirements-v0-scope.md` §3 RR7
8
+
9
+ ---
10
+
11
+ ## 1 目标
12
+
13
+ 把"非 serenity 目录无法被 plugin 识别"的**死循环**打破:用户在任何 git 仓内运行 `/serenity-init` 即可把它转成 serenity 实例。
14
+
15
+ ---
16
+
17
+ ## 2 范围
18
+
19
+ ### 2.1 在范围内
20
+
21
+ | # | 项 | 备注 |
22
+ |---|----|------|
23
+ | 1 | TUI plugin 注册 `/serenity-init` slash command | 入口点 |
24
+ | 2 | DialogPrompt 让用户输入 prefix | 智能预填默认值 |
25
+ | 3 | 写 `/.serenity` + `git add` + `git commit` | RR7 ④ ⑤ 行为 |
26
+ | 4 | 失败用 `api.ui.toast` 通知(variant=error) | 不抛错给 TUI |
27
+ | 5 | 成功后 toast 提示"请重启 opencode" | 不做 live re-activation(v0) |
28
+ | 6 | `initSerenity(cwd, prefix)` 纯函数 | 单元测试友好 |
29
+
30
+ ### 2.2 在范围外(v1+ 候选)
31
+
32
+ - **Live re-activation**(成功后 plugin 立即变 active):state machine 需加 `disabled → loading → ready` 转移路径。v0 不做,提示用户重启。
33
+ - **Server `serenity_init` tool**(让 LLM/脚本可调):handler 复用 `initSerenity` 纯函数,但 TUI slash 已是主入口,v0 跳过。
34
+ - **多实例并存 / 重命名**(RR7 spec "可选更新"分支):v0 默认 no-op,提示"已是 serenity"。
35
+ - **Slash args 直通**:`/serenity-init my-prefix` 在 TUI 中 args 被丢弃(`keymap.tsx:279` `dispatchCommand` 不传 args),所以 v0 永远走 dialog。
36
+
37
+ ---
38
+
39
+ ## 3 命名模型(RR7 ② 精修)
40
+
41
+ | 元素 | 旧 RR7 ② 描述 | 精修后 |
42
+ |------|----------------|--------|
43
+ | 用户输入 | 实例名(kebab-case) | **prefix**(不带后缀) |
44
+ | 最终实例名 | = 用户输入 | `= ${prefix}-serenity` |
45
+ | 例子 | `my-cool-project` | `xx` → `xx-serenity`;`tg` → `tg-serenity` |
46
+
47
+ **为什么精修**:让所有 serenity 实例**天然带 `-serenity` 后缀**,便于:
48
+ - 一眼区分 serenity vs 普通 skill
49
+ - 与 opencode 自带 skill(`home-*`)的命名风格不同
50
+ - prefix 是用户可控的短字符串(≤ 20 字符),dialog 输入更轻
51
+
52
+ **默认 prefix 计算**(`defaultPrefix(cwdRoot)`):
53
+
54
+ | 目录名 | 推导 prefix |
55
+ |--------|-----------|
56
+ | `tg-serenity` | `tg`(剥后缀) |
57
+ | `xx-serenity` | `xx`(剥后缀) |
58
+ | `my-cool-project` | `my-cool-project`(kebab-case 转换) |
59
+ | `MyProject` | `myproject`(kebab-case 转换) |
60
+ | `My Cool App` | `my-cool-app`(kebab-case 转换) |
61
+
62
+ > 目录名后缀检测必须要求 prefix 部分自身是合法 kebab-case(`^[a-z0-9]+(-[a-z0-9]+)*$`),避免 `---serenity` 之类误判。
63
+
64
+ ---
65
+
66
+ ## 4 触发 + UX 流程
67
+
68
+ ```
69
+ 用户输入: /serenity-init
70
+ ↓ TUI slash 识别
71
+ onSelect(dialog) 触发
72
+ ↓ 检查 dialog 非空
73
+ dialog.replace(() => <DialogPrompt
74
+ title: "Initialize serenity"
75
+ placeholder: "kebab-case prefix (e.g. xx, tg)"
76
+ value: defaultPrefix(cwdRoot) ← 智能预填
77
+ onConfirm(value):
78
+ prefix = value.trim()
79
+ 验证 prefix (kebab-case) ─────→ 失败: toast error, dialog 保持
80
+ ↓ 通过
81
+ initSerenity(cwd, prefix):
82
+ - 查 git repo
83
+ - 查 /.serenity 已存在?
84
+ - 是 → return { kind: 'already', name }
85
+ - 否 → write /.serenity + git add + commit
86
+ - 失败抛错(带 rollback)
87
+ ↓ 成功
88
+ dialog.clear()
89
+ toast: "initialized (instance: xx-serenity); please restart opencode"
90
+ onCancel:
91
+ dialog.clear()
92
+ toast: "init cancelled"
93
+ >)
94
+ ```
95
+
96
+ **关键决策**:
97
+ - **Dialog 在错误时**保持开启**(invalid prefix / git error)**:让用户能改完重试,不强制从头开始
98
+ - **Dialog 在成功 / 已是 / 取消时关闭**:减少认知负担
99
+ - **args 被忽略**:`/serenity-init my-prefix` 与 `/serenity-init` 走同一流程
100
+
101
+ ---
102
+
103
+ ## 5 失败矩阵
104
+
105
+ | 条件 | 行为 | 错误类 / 返回 |
106
+ |------|------|---------------|
107
+ | cwd 不在 git repo | toast: "cwd is not a git repo; please run `git init` first, then `/serenity-init` again" | throw `NotInGitRepoError` |
108
+ | prefix 不合法(空 / 非 kebab-case) | toast: "Invalid name \"X\": must be kebab-case (a-z, 0-9, dashes; no leading/trailing dash)" | throw `InvalidInstanceNameError` |
109
+ | `/.serenity` 已存在 | dialog 关闭;toast: "already a serenity directory (instance: N); no changes made" | return `{ kind: 'already', instanceName }` |
110
+ | `git add` 失败 | rollback(删 `/.serenity`);toast error | throw `InitGitCommitError` |
111
+ | `git commit` 失败 | 同上(rollback) | throw `InitGitCommitError` |
112
+ | `dialog` 为 undefined | toast error,return | (罕见,防御性) |
113
+
114
+ ---
115
+
116
+ ## 6 API 设计
117
+
118
+ ### 6.1 `src/util/init.ts`(新文件)
119
+
120
+ ```ts
121
+ export const SERENITY_SUFFIX = '-serenity';
122
+
123
+ export type InitResult =
124
+ | { kind: 'created'; instanceName: string; prefix: string; cwdRoot: string }
125
+ | { kind: 'already'; instanceName: string; cwdRoot: string };
126
+
127
+ /** 字符串 → kebab-case(lowercase, 非 alnum → -, 折叠)*/
128
+ export function toKebabCase(s: string): string;
129
+
130
+ /** 剥 "-serenity" 后缀;不在则原样返回 */
131
+ export function stripSerenitySuffix(name: string): string;
132
+
133
+ /** 验证 prefix 是 kebab-case;不合法抛 InvalidInstanceNameError */
134
+ export function validatePrefix(prefix: string): void;
135
+
136
+ /** 智能默认 prefix(见 §3 表格) */
137
+ export function defaultPrefix(cwdRoot: string): string;
138
+
139
+ /** 纯函数:初始化 cwd 为 serenity 实例 */
140
+ export function initSerenity(cwd: string, prefix: string): InitResult;
141
+ ```
142
+
143
+ ### 6.2 `src/util/serenity-file.ts`(新增 2 个函数)
144
+
145
+ ```ts
146
+ /** 写 `/.serenity`(创建或覆盖),内容 = instanceName + '\n' */
147
+ export function writeSerenityFile(cwdRoot: string, instanceName: string): void;
148
+
149
+ /** 删 `/.serenity`(init 失败时 rollback 用),ENOENT 静默 */
150
+ export function removeSerenityFile(cwdRoot: string): void;
151
+ ```
152
+
153
+ ### 6.3 `src/errors.ts`(新增 1 个错误类)
154
+
155
+ ```ts
156
+ export class InvalidInstanceNameError extends SerenityError {
157
+ constructor(name: string) { ... }
158
+ }
159
+ ```
160
+
161
+ (`NotInGitRepoError` / `InitGitCommitError` 已存在,直接复用)
162
+
163
+ ### 6.4 `src/tui.ts`(重写)
164
+
165
+ ```ts
166
+ const Tui: TuiPlugin = async (api) => {
167
+ // 1. 激活 toast(v1.9.1 保留)
168
+ api.ui.toast({ ... });
169
+
170
+ // 2. 注册 /serenity-init slash command(v1.10 RR7)
171
+ api.command?.register(() => [{
172
+ title: 'serenity: init cwd',
173
+ value: 'serenity-init',
174
+ description: 'Create /.serenity and git-commit (requires restart)',
175
+ slash: { name: 'serenity-init' },
176
+ onSelect: (dialog) => { /* 见 §4 流程 */ },
177
+ }]);
178
+ };
179
+ ```
180
+
181
+ **注意**:
182
+ - 不用 `createElement`(v1.16.2 SDK 中 `api.ui.DialogPrompt` 是函数,**直接调用**即可,绕过 JSX 编译时 transform 问题)
183
+ - 走 `dialog.replace(() => api.ui.DialogPrompt({...}))` 模式,与 S019 调查结论一致
184
+
185
+ ---
186
+
187
+ ## 7 测试矩阵
188
+
189
+ ### 7.1 `tests/util-init.test.ts`(新文件,~10 tests)
190
+
191
+ | # | 测试 | 覆盖 |
192
+ |---|------|------|
193
+ | 1 | `toKebabCase` 各种输入 | helper |
194
+ | 2 | `stripSerenitySuffix` 命中 / 不命中 | helper |
195
+ | 3 | `validatePrefix` 接受 / 拒绝 | helper |
196
+ | 4 | `defaultPrefix` 各种目录名 | helper |
197
+ | 5 | `initSerenity` 完整成功流程 | 写文件 + git commit + 读回 |
198
+ | 6 | `initSerenity` 已存在 → `{ kind: 'already' }` | 失败矩阵 |
199
+ | 7 | `initSerenity` invalid prefix 抛 `InvalidInstanceNameError` | 失败矩阵 |
200
+ | 8 | `initSerenity` 非 git repo 抛 `NotInGitRepoError` | 失败矩阵 |
201
+ | 9 | `initSerenity` git commit 失败时 rollback(文件被删) | rollback 验证 |
202
+ | 10 | `initSerenity` 不创建 `.opencode/skills/<N>/` | RR7 ⑤ |
203
+
204
+ ### 7.2 `tests/util-serenity-file.test.ts`(+2 tests)
205
+
206
+ - `writeSerenityFile` 写盘 + 读回
207
+ - `removeSerenityFile` 存在/不存在都安全
208
+
209
+ ### 7.3 `tests/tui.test.ts`(+2 tests)
210
+
211
+ - 验证 slash command 已被注册(mock `api.command.register`)
212
+ - 验证 onConfirm 成功路径触发 `initSerenity` + toast
213
+
214
+ ### 7.4 `tests/errors.test.ts`(+1 line)
215
+
216
+ - `InvalidInstanceNameError` 加入 `all extend SerenityError` 列表
217
+
218
+ ---
219
+
220
+ ## 8 文件变更总览
221
+
222
+ | 文件 | 变更类型 | 摘要 |
223
+ |------|----------|------|
224
+ | `docs/rr7-init-design.md` | 新建 | 本文档 |
225
+ | `docs/requirements-v0-scope.md` | 修改 | RR7 ② 改为 prefix 模型(§3 命名模型) |
226
+ | `src/util/init.ts` | 新建 | RR7 init 纯函数 + 4 个 helper |
227
+ | `src/util/serenity-file.ts` | 修改 | + `writeSerenityFile` / `removeSerenityFile` |
228
+ | `src/errors.ts` | 修改 | + `InvalidInstanceNameError` |
229
+ | `src/tui.ts` | 重写 | + slash command 注册(保留 toast) |
230
+ | `tests/util-init.test.ts` | 新建 | §7.1 测试矩阵 |
231
+ | `tests/util-serenity-file.test.ts` | 修改 | §7.2 |
232
+ | `tests/tui.test.ts` | 修改 | §7.3 |
233
+ | `tests/errors.test.ts` | 修改 | §7.4 |
234
+ | `SESSION.md` | 修改 | v1.10 记录(实施后) |
235
+
236
+ ---
237
+
238
+ ## 9 决策记录
239
+
240
+ | # | 决策 | 理由 |
241
+ |---|------|------|
242
+ | D1 | 用户输入 prefix,plugin 加 `-serenity` 后缀 | 用户 m0040 明确要求;统一命名风格 |
243
+ | D2 | 用 `api.ui.DialogPrompt`(直接调用),不写 JSX | v1.9.1 已确认 `@opentui/solid` JSX runtime 不可用;函数直调等价 |
244
+ | D3 | Dialog 在错误时**不关闭** | 让用户能改完重试 |
245
+ | D4 | 成功 / 已是 / 取消时**关闭** Dialog | 减少认知负担 |
246
+ | D5 | 不做 live re-activation | 状态机变更面较大,v0 提示用户重启最简 |
247
+ | D6 | "已存在"走 no-op(不更新实例名) | 重命名隐含 `.opencode/skills/<N>/` 重建,超出 init 范围 |
248
+ | D7 | `git add` 失败要 rollback 写文件 | 保持 cwd 一致状态(不留半成品) |
249
+ | D8 | args 路径**不实现** | SDK 不支持 slash arg 传递(keymap.tsx:279) |
250
+ | D9 | 不在 v0 加 server `serenity_init` tool | 入口已覆盖;tool 复用纯函数但增量价值小 |
251
+ | D10 | 错误抛 `InvalidInstanceNameError`(不返回) | 真正的非法状态,应该 throw;"已存在"才是软失败 |
252
+
253
+ ---
254
+
255
+ ## 10 未决 / 已决
256
+
257
+ | 项 | 状态 |
258
+ |----|------|
259
+ | Slash command 路径 | ✅ TUI onSelect + DialogPrompt |
260
+ | 实例命名 | ✅ prefix + `-serenity` 后缀 |
261
+ | Live re-activation | ⏸ 推迟 v1+ |
262
+ | Server tool | ⏸ 推迟 v1+ |
263
+ | 失败 UX | ✅ toast variant=error,dialog 错误时保持 |
264
+ | Rollback 策略 | ✅ 写文件后 git 失败 → 删文件 |
265
+ | **v1.10.1 全局可见** | ✅ TUI plugin 自安装到 `~/.config/opencode/tui.json`,slash command 在**任何**目录可见(详见 §11)|
266
+
267
+ ---
268
+
269
+ ## 11 v1.10.1 — slash command 全局可见
270
+
271
+ > 状态:✅ 实施(commit 待生成;与本文档同步更新)
272
+
273
+ ### 11.1 根因
274
+
275
+ TUI plugin 只在 `tui.json` 文件里登记的路径下被 opencode 加载。plugin 路径只登记在项目 tui.json(`<serenity-root>/tui.json`),非 serenity 目录 walk-up 找不到 tui.json → plugin 不加载 → `Tui(api)` 永不调 → slash command 不出现。
276
+
277
+ 机制文件:
278
+ - `packages/opencode/src/config/paths.ts:10-21`(`ConfigPaths.files` walk-up 找 tui.json)
279
+ - `packages/opencode/src/cli/cmd/tui/config/tui.ts:194-231`(合并 config)
280
+ - `packages/opencode/src/cli/cmd/tui/plugin/runtime.ts:1074-1129`(从 `config.plugin_origins` 加载 plugin)
281
+
282
+ ### 11.2 修复
283
+
284
+ plugin `Tui(api)` 入口**自检并自安装**到 global TUI config(`$XDG_CONFIG_HOME/opencode/tui.json` 或 `~/.config/opencode/tui.json`):
285
+
286
+ - 幂等:plugin path 已在 list 中时 no-op
287
+ - 保留其他字段(theme / keybinds / attention / prompt / …)
288
+ - 写失败不抛:返回 `{ changed: false, error }`,仅 log.warn
289
+ - 路径规范化:所有 path 走 realpathSync + pathToFileURL,与 opencode 的 `ConfigPlugin.resolvePluginSpec` 一致
290
+
291
+ ### 11.3 行为契约
292
+
293
+ - **首次启动**(plugin 第一次被加载):写入 global tui.json + 弹 toast "restart opencode to enable /serenity-init in non-serenity directories"(与 D5 一致)
294
+ - **后续启动**(plugin 已 global 注册):no-op,无额外 toast
295
+ - **self-install 失败**(permission denied / 磁盘满 / …):slash command 仍注册,仅 log.warn(plugin "dormant" 状态下也可用)
296
+ - **D1-D10 不动**:本节是 v1.10 RR7 设计的"可见性补丁",不动 UX / 命名 / 失败矩阵
297
+
298
+ ### 11.4 关联
299
+
300
+ - 代码:`src/util/tui-install.ts`(新)+ `src/tui.ts`(B 段接入)
301
+ - 测试:`tests/tui-install.test.ts`(23 unit tests)+ `tests/tui.test.ts`(+5 integration tests)
302
+ - 调研 SESSION:`AGENT_SESSIONS/2026-06-06--S020--fix-serenity-init-visibility/`
303
+
304
+ > 本文档是 RR7 实施的 source of truth。任何对触发流程 / 命名 / 失败矩阵的修订都应改本文档 + 在 SESSION.md 留 commit 历史。
@@ -0,0 +1,132 @@
1
+ # v0.1 Candidates — Reference Plugins Code Analysis ✅ ALL DONE
2
+
3
+ > **Created**: 2026-06-05
4
+ > **Status**: ✅ **3 候选全部已实施** (v0.0.2 — 2026-06-07)
5
+ > **Source**: 代码级对照(`/tmp/opencode-refs/`)
6
+
7
+ 调研参考 plugin 后的 v0.1 改进候选。所有方案均**基于 oMo / skillful 验证过的设计模式**,不引入未经验证的新设计。
8
+
9
+ ## 参考 plugin 一览
10
+
11
+ | 仓库 | 状态 | 规模 | 核心价值 |
12
+ |------|------|------|---------|
13
+ | **oh-my-opencode** (code-yeongyu/oh-my-openagent) | 活跃 | 61K stars / 7,557 commits | hook 工厂分层 + Hashline Edit 独立包 |
14
+ | **opencode-skillful** (zenobi-us) | **archived 2026-02-14** | 305 stars | 两阶段 init + pre-indexed resources 防 path traversal |
15
+
16
+ ## 3 个 v0.1 候选(基于代码分析)
17
+
18
+ ### 候选 1:两阶段 init(skillful 模式)
19
+
20
+ **参考源码**:`opencode-skillful/src/ReadyStateMachine.ts`(91 行)+ `src/index.ts`(177 行)
21
+
22
+ **当前 v0 痛点**(`src/activation.ts`):
23
+ - `tryActivate()` 一次性 await 完所有 IO(git root / `/.serenity` / skill path)
24
+ - opencode 启动阻塞等待激活完成
25
+ - IO 慢时用户体验差
26
+
27
+ **参考实现**:
28
+ ```ts
29
+ // skillful src/index.ts:177
30
+ export const SkillsPlugin: Plugin = async (ctx) => {
31
+ const config = await getPluginConfig(ctx);
32
+ const api = await createApi(config);
33
+ api.registry.initialise(); // ← 不 await,fire-and-forget
34
+ return { tool: { skill_use, skill_find, skill_resource } };
35
+ };
36
+
37
+ // Tool execute 内:
38
+ execute: async (args, toolCtx) => {
39
+ await api.registry.controller.ready.whenReady(); // ← 阻塞等 phase 2
40
+ // ... 真正逻辑
41
+ }
42
+ ```
43
+
44
+ **v0.1 改进方案**:
45
+ - `tryActivate` 改为 fire-and-forget:返回 hooks 立即生效
46
+ - `activationState` 用 `ReadyStateMachine`(loading → ready / disabled)
47
+ - 每个 tool execute 第一行 `await state.whenReady()` 阻塞
48
+ - **优点**:opencode 启动快;RR6 验证在后台跑
49
+ - **风险**:LLM 可能在 tryActivate 完成前尝试调 msm_list — 用 throw 友好提示
50
+ - **预计**:~30 行新增
51
+
52
+ ### 候选 2:Pre-indexed resources 轻量版(skillful 模式)
53
+
54
+ **参考源码**:`opencode-skillful/src/SkillRegistry.ts`(421 行)
55
+
56
+ **当前 v0 痛点**(`src/msm.ts`):
57
+ - `msm_exec` 接受 `args.command` 任意字符串
58
+ - 路径 escape 检查靠 `isPathInside` 黑名单 + args 解析时手动校验
59
+ - **漏洞**:args 里出现 `../../../etc/passwd` 这种 substring 检查不一定覆盖
60
+
61
+ **参考实现**:
62
+ - `SkillRegistry` 解析每个 SKILL.md 时**预索引**所有 `scripts/` / `assets/` / `references/`
63
+ - 存为 `Map<relativePath, absolutePath>`
64
+ - Tool 接受**相对路径** → resolver 只在 map 查 → **无法 path traversal**
65
+
66
+ **v0.1 改进方案**(轻量):
67
+ - msm registry 加载后**预解析**每个 msm 的参数 schema
68
+ - 标记"路径型参数"(如 `filePath`)
69
+ - `msm_exec` 阶段对路径型参数做 `isPathInside(cwdRoot, value)` 强制检查
70
+ - **优点**:把"路径检查"从"参数值解析"提到"参数 schema 声明"层
71
+ - **预计**:~80 行新增
72
+
73
+ **v1 完整方案**(更彻底,**不**放 v0.1):
74
+ - LLM 永远传相对路径,plugin 解析为绝对路径(LLM 不知道)
75
+ - 完全免疫 path traversal
76
+ - 预计 ~200 行
77
+
78
+ ### 候选 3:Hook 工厂分层(oMo 模式)
79
+
80
+ **参考源码**:`oh-my-opencode/src/testing/create-plugin-module.ts`(5 工厂分层)
81
+
82
+ **当前 v0 痛点**(`src/permission.ts`):
83
+ - 所有 logic 塞一个文件
84
+ - hook 之间强耦合,单 hook 失败可能影响其他
85
+ - 不易独立测试
86
+
87
+ **参考实现**:
88
+ ```ts
89
+ // oMo createPluginModule 5 工厂:
90
+ const managers = createManagers(ctx);
91
+ const tools = createTools({ ctx, managers });
92
+ const hooks = createHooks({ ctx, pluginConfig, isHookEnabled, safeHookEnabled });
93
+ const tmux = createRuntimeTmuxConfig({ ctx, pluginConfig });
94
+ const pluginInterface = createPluginInterface({ ... });
95
+ ```
96
+
97
+ **v0.1 改进方案**:
98
+ - 拆 `createPermissionGuards`(path guard + bash 防御)+ `createSessionCompacting`(inject RR3/RR7 提示)+ `createShellEnv`(HOME_SERENITY_ROOT 注入)
99
+ - `isHookEnabled(hookName)` 集中开关
100
+ - `safeCreateHook` 包装 try/catch
101
+ - **优点**:模块化 + 单 hook 失败隔离 + 可独立测试
102
+ - **预计**:~100 行重构
103
+
104
+ ## v1 候选(不放在 v0.1)
105
+
106
+ | 改进 | 复杂度 | 价值 | 状态 |
107
+ |------|:---:|------|------|
108
+ | **Pre-indexed resources 完整版** | ~200 行 | 安全 ↑↑ | ⏸ v0.0.3+ 候选(Open follow-ups #1)|
109
+ | **Hashline Edit 集成** | ~100 行 | LLM edit 成功率 ↑↑ | ⛔ v1-2 已实现(`src/hashline/`,commit `e39ed23`),但实测 LLM 用得糟糕,v1.4 后撤回;详见 index.ts:19 注释 |
110
+
111
+ ## v0.1 实施总览 ✅ 全部完成
112
+
113
+ | 项 | 实施 commit | 实施时间 | 测试 |
114
+ |---|-----------|---------|:---:|
115
+ | 候选 1:两阶段 init | `fc1f6a7` (v0.1-1) | 2026-06-05 | 13 tests |
116
+ | 候选 2:Pre-indexed 轻量 | `1c4ce6b` (v0.1-2) | 2026-06-05 | 4 tests |
117
+ | 候选 3:Hook 工厂分层 | `ca4360f` (v0.1-3) | 2026-06-05 | 9 tests |
118
+ | **合计** | 3 commits | 1 天 | 26 new tests |
119
+
120
+ 实施后总测试数 320/320 pass(v0.0.2 状态)。
121
+
122
+ ## 不采用项
123
+
124
+ - **oh-my-opencode 完整 5 工厂** — 过度设计,v0 plugin 规模不到 500 行,5 工厂分层得不偿失
125
+ - **skillful 的 archived** — 不能再跟随上游演进,**只借鉴模式不依赖代码**
126
+
127
+ ## 关联文档
128
+
129
+ - `docs/requirements-v0-scope.md` — RR1-RR7 范围层
130
+ - `docs/architecture-v0.md` — 方案层 10 步启动
131
+ - `docs/contract-v0.md` — 接口层 6 契约
132
+ - `SESSION.md` — 项目跟进载体(v0.1 未决问题清单)