dsh-offhours 0.1.4 → 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.
- package/README.md +53 -0
- package/bin/offhours.mjs +22 -0
- package/feature-worktrees/feature-lib.sh +41 -19
- package/lib/client.js +17 -0
- package/lib/config.js +32 -4
- package/mechanism/ns.py +107 -8
- package/mechanism/scan.sh +82 -3
- package/mechanism/selftest.py +23 -2
- package/mechanism/superagent_bridge.py +426 -0
- package/package.json +5 -4
- package/panel/client.js +17 -0
- package/panel/index.js +36 -8
- package/panel/locale/en.json +2 -1
- package/panel/locale/zh.json +2 -1
- package/feature-worktrees/README.md +0 -361
- package/mechanism/README.md +0 -266
- package/panel/cordis.patch.yml +0 -3
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
# feature-worktrees —— 特性 worktree:会话主动关联 + 生命周期 + 提交强制
|
|
2
|
-
|
|
3
|
-
> 起因(用户 2026-10-13):`/root/hemou` 里堆了近 20 个 `devloop-round-N` 分支,
|
|
4
|
-
> 仓库目录名是工程名、分支名是轮次号,**看不出在开哪个特性,也没法并行管理**;
|
|
5
|
-
> 且要求「每次 hook 执行的 commit 都落在这个特性对应的 worktree 上」,并且要**跨工程**。
|
|
6
|
-
> 第二轮追加要求:钩子要**关联一个处理 worktree 的 skill**,会话动手前必须**主动关联**到 worktree,
|
|
7
|
-
> 并补齐三组生命周期操作——基础(新建/切换/恢复)、上下文恢复、回收(合并主干 + 回收 worker)。
|
|
8
|
-
|
|
9
|
-
## 0. 模型:worker ↔ worktree
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
会话(worker)──关联(按会话,DSH_SESSION_ID)──▶ 特性 ──┬─ 工程 A 的 worktree(feat/<特性名>)
|
|
13
|
-
└─ 工程 B 的 worktree(feat/<特性名>)
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
- **关联是按会话的**:`/root/features/.sessions/<会话id>.json`。两个会话可以各开各的特性,互不抢
|
|
17
|
-
(全局 `.active` 只是兜底/展示——第一版用它做判据,实测两个会话会互相抢)。
|
|
18
|
-
- **位置即关联**:会话 cwd 落在某 worktree 内时,等于已关联该特性,无需额外动作。
|
|
19
|
-
- **动手前先关联**:不关联又去改「已登记工程的主检出」,钩子会拦下并告诉你该去哪(见 §3)。
|
|
20
|
-
- 规范随技能下发:`~/.dsh/skills/feature-worktree/`(主机级,本机所有会话的 skill 目录里都能看到)。
|
|
21
|
-
|
|
22
|
-
## 1. 布局(A 方案:特性在前、工程在后)
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
/root/features/
|
|
26
|
-
.sessions/<会话id>.json 会话 ↔ 特性 绑定(谁在哪个 worktree 上干活)
|
|
27
|
-
.repos.json 工程主检出登记表 {工程名: 主检出路径}
|
|
28
|
-
.active 兜底/展示用的当前特性(不再是判据)
|
|
29
|
-
<特性名>/
|
|
30
|
-
feature.json 元数据(参与工程 / 主检出 / worktree / 分支 / 状态)
|
|
31
|
-
journal.md 工作日志(追加型:这个 worktree 干了什么、下一手是什么)
|
|
32
|
-
<工程名>/ 该工程在本特性下的 git worktree(分支统一 feat/<特性名>)
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
- **特性名是人可读的 kebab-case**(如 `ai-coding-doc-consistency`),替代 `devloop-round-N`。
|
|
36
|
-
- `ls /root/features` 就是「我在开哪些特性」的答案;一个特性跨多个工程 = 同一目录下多个子目录。
|
|
37
|
-
- 工程主检出留在 `/root/<工程名>`,只用于主干与合并。
|
|
38
|
-
|
|
39
|
-
## 2. 命令(`feature`,装在 /root/.local/bin):四组操作
|
|
40
|
-
|
|
41
|
-
### 2.1 关联(动手前的第一步)
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
feature status # 我这个会话现在关联在哪、那个 worktree 什么状态
|
|
45
|
-
feature attach <特性名> [工程名] # 关联(写 .sessions/<会话id>.json)
|
|
46
|
-
feature switch <特性名> [工程名] # 切换:解除旧关联再关联新的
|
|
47
|
-
feature detach # 解除关联
|
|
48
|
-
feature new <特性名> --repos a,b # 新建 worktree,**并自动关联本会话**
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### 2.2 基础:新建 / 切换 / 恢复
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
feature new <特性名> --repos <工程名,...> [--title "..."] [--from <ref>]
|
|
55
|
-
feature resume <特性名> [工程名] # 恢复:关联 + 打印该 worktree 的完整上下文
|
|
56
|
-
feature list [--all|--branches] # 在用特性 / 含已回收 / 附未收编分支(✓=已并入 main)
|
|
57
|
-
feature path <特性名> [工程名] # 打印 worktree 路径
|
|
58
|
-
feature check --tool edit --file <p> # 用与钩子**同一套**规则判定(放行 0 / 阻断 2)
|
|
59
|
-
feature repo add <工程名> <主检出路径>;feature repo list;feature doctor
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### 2.3 上下文恢复:这个 worktree 干了什么
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
feature context [特性名] [--full] # 分支/相对 main 的提交与改动规模/未提交/最近自动存档/日志/相关笔记与看板
|
|
66
|
-
feature journal <特性名> "<一句话>" # 追加工作日志(意图与结论;事实以 git log 为准)
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### 2.4 回收:验收后合并回主干、回收 worker
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
feature recycle <特性名> # 只打印计划(预检 + 将执行的动作)
|
|
73
|
-
feature recycle <特性名> --yes [--push] [--delete-branch] [--keep-worktree] [--no-redirect]
|
|
74
|
-
feature close <特性名> [--delete-branch] [--force] # 放弃:只拆 worktree,不合并
|
|
75
|
-
feature prune <特性名|--all> [--purge] [--yes] # 清理已回收路标 / 墓碑
|
|
76
|
-
feature relink <特性名|--all> [--yes] # 给已回收特性补/重建路标
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
`recycle` 的动作:预检(worktree 干净、主检出干净)→ 主检出 `checkout main` →
|
|
80
|
-
`merge --no-ff feat/<特性名>` → 移除 worktree **并在原地留路标**(见 §2.6)→(可选)删分支/推主干 →
|
|
81
|
-
标记 `status=recycled`(记 `recycledAt`/`mergeCommit`/`retiredTo`)→ **解除相关会话关联**。
|
|
82
|
-
冲突时 `merge --abort` 并要求人工处理;默认**不推主干**。
|
|
83
|
-
|
|
84
|
-
`close`(放弃)刻意**不留路标**——代码没合进 main,指向 main 等于把人带错地方;它留的是
|
|
85
|
-
`CLOSED.md` 墓碑:代码在哪个分支、`git -C <主检出> show <分支>:<路径>` 怎么读出来。
|
|
86
|
-
|
|
87
|
-
### 2.6 路标:回收后旧路径仍可读(2026-10-15)
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
/root/features/<特性>/<工程> → /root/<工程> (符号链接 = 只读门牌)
|
|
91
|
-
旧路径(会话引用过) 代码现在住的地方(合并后的 main)
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**起因(用户现场)**:会话是长命的,而回收原来是把 worktree 目录**拆掉**——对话里已经写下、
|
|
95
|
-
`@` 引用过、甚至当会话工作区用的那些绝对路径一夜之间全变死链(本机 `facet-constitution` /
|
|
96
|
-
`facet-contract` 两个已回收特性当时正处于该状态,任何开在那里的会话都读不到自己的文件)。
|
|
97
|
-
修法不是事后补救,而是改「回收」的语义:**搬家并留下门牌**,而不是销毁。
|
|
98
|
-
|
|
99
|
-
| 动作 | 命令 | 保证 |
|
|
100
|
-
| --- | --- | --- |
|
|
101
|
-
| 读旧路径 | 直接 `cat` / `@路径` / `grep` | 放行(内容 = 主检出当前状态) |
|
|
102
|
-
| 写旧路径 | `edit`/`write`/`git commit` | **拦**(规则 2c);git 会把路标解析回主检出 |
|
|
103
|
-
| 清理 | `feature prune <特性名> --yes` | 只 `rm` 符号链接;目标是真实目录就拒绝 |
|
|
104
|
-
| 补回 | `feature relink <特性名\|--all> --yes` | 早期回收/被 prune 删掉的路标重新补上 |
|
|
105
|
-
| 连记录清掉 | `feature prune <特性名> --purge --yes` | journal 保留为 `<特性名>.history-<时间>.md` |
|
|
106
|
-
|
|
107
|
-
`retiredTo`/`retiredAt` 同时记进 `feature.json`(注册表事实),路标本身是磁盘事实——`list --all`、
|
|
108
|
-
`context`、`doctor`、`audit` 都按「已回收 + 路标 → 主检出」显示,而不是报 `[缺失]`。
|
|
109
|
-
`session-start.sh` 也不再把这些特性报成「在用特性」:只报在用的,已回收的另起一行并说明写到那里会被拦。
|
|
110
|
-
|
|
111
|
-
### 2.7 未值守:按需创建 worktree + 权限预检(2026-10-15)
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
feature ensure <特性名> --repos <工程名,...> [--title ...] # 幂等入口:不在就建、在就核对+关联本会话
|
|
115
|
-
feature preflight [--repos <工程名,...>] [--quiet] # 权限体检:免批准就能建 worktree 吗
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
**要解决的事**(用户现场):会话一旦需要 worktree 就该**按需创建**,而且那两处目录必须提前具备权限;
|
|
119
|
-
不能让「未值守跑批」跑到一半才发现要人批准。当天 23:16 的真实失败正是两连击:
|
|
120
|
-
`/root/features is read-only`(沙箱)→ 改用 DSH 自带 `create_worktree` → `unknown option: --no-lazy-fetch`。
|
|
121
|
-
|
|
122
|
-
**为什么 `workspace-write` 下这件事结构上做不到**(不是配置没调好):
|
|
123
|
-
|
|
124
|
-
| 建一个 worktree 要写的地方 | 在不在会话工作区内 |
|
|
125
|
-
| --- | --- |
|
|
126
|
-
| `/root/features/<特性>/<工程>/`(新目录 + 检出) | ✗ |
|
|
127
|
-
| `<repo>/.git/worktrees/<名>/` 与共享 git 元数据(连 worktree 里的 `git commit` 也写这里) | ✗ |
|
|
128
|
-
|
|
129
|
-
而 `workspace-write` 只有**一个**可写根:`dsh-sandbox-local` 的 bwrap 档是
|
|
130
|
-
`--ro-bind / /` + 仅 `--bind <workspaceRoot>` + `--tmpfs /tmp`(`writableRoots` = 工作区 + `/tmp`
|
|
131
|
-
+ tmpdir,只有这三项);`dsh-sandbox-policy` 也只认 `mode`/`workspaceRoot`,没有额外可写路径白名单。
|
|
132
|
-
**实测**(用 `bwrap` 复刻同一配方):`workspace-write` → 特性根不可写、git 元数据不可写;
|
|
133
|
-
`danger-full-access` → 可写。
|
|
134
|
-
|
|
135
|
-
**本机的处置**(放开沙箱,边界改由 `feature-guard` 承担):
|
|
136
|
-
|
|
137
|
-
| 对象 | 生效点 | 是否需要重启 |
|
|
138
|
-
| --- | --- | --- |
|
|
139
|
-
| 交互/网页会话(dsh-web) | `/etc/systemd/system/dsh-web.service.d/30-permission-mode.conf` 的 `Environment=DSH_PERMISSION_MODE=danger-full-access` | 需要(`daemon-reload` + 重启 `dsh-web`) |
|
|
140
|
-
| 未值守 worker(nightshift 用 `dsh --profile headless`) | `/root/.dsh/profiles/headless/cordis.patch.yml` 里对 `sandbox-policy` 的覆盖 | 不需要(每次启动组合 profile) |
|
|
141
|
-
|
|
142
|
-
`dsh-base` 用同一个变量决定沙箱模式**与** approval:`danger-full-access ⇒ approval never`,
|
|
143
|
-
所以这一处同时消掉「需要批准」这个死结。放开后的边界:feature-guard 钩子(登记工程主检出上的
|
|
144
|
-
写与提交一律拦、已回收路标只读)、`scan.sh` 的机械约束、以及 `feature preflight` 这个可执行体检。
|
|
145
|
-
|
|
146
|
-
`preflight` 做的是**真探针而不是推断**:真写 `/root/features`、真写 `<repo>/.git`、真跑一次
|
|
147
|
-
`git worktree add --no-checkout` 再 `remove`;失败会打印确切修法。它已并入 `feature doctor`。
|
|
148
|
-
|
|
149
|
-
**DSH 自带的 `create_worktree` 工具已在本机停用**(web profile patch 里 `- id: tool-worktree /
|
|
150
|
-
disabled: true`):它硬要求 git ≥2.45 并传 `--no-lazy-fetch`(本机 git 2.43.0 必失败),而且它的
|
|
151
|
-
目录布局绕过 `/root/features/<特性>/<工程>` 的注册表与守卫。按需建一律走 `feature ensure` / `feature new`。
|
|
152
|
-
|
|
153
|
-
### 2.5 运维:体检与「把主检出里的滞留改动搬走」
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
feature audit # 只读体检(可复跑):主检出分支/未提交、各 worktree 的改动与领先落后、
|
|
157
|
-
# 未推送、已并入 main / 被取代(给出确切回收命令)、孤儿 worktree
|
|
158
|
-
feature rescue <特性名> [--project 工程] # 只打印计划
|
|
159
|
-
feature rescue <特性名> --yes # 把主检出里滞留的未提交改动搬进该特性的 worktree
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
`rescue` 的存在理由:主检出里可能压着**换制度之前**留下的未提交改动,而主检出现在
|
|
163
|
-
**不许改也不许提交**(规则 3)。做法是 `stash push -u`(含未跟踪)→ 在目标 worktree
|
|
164
|
-
`stash apply`(stash 是**仓库级**的,跨 worktree 可用)→ **保留 stash 作备份**,核实后由人
|
|
165
|
-
`git stash drop`。前置:目标 worktree 必须干净(否则两堆改动会打架,它会拒绝并要求先处理)。
|
|
166
|
-
|
|
167
|
-
## 3. 强制规则(`feature-guard.sh`,主机级 PreToolUse 钩子)
|
|
168
|
-
|
|
169
|
-
判定顺序(**宁可放行,唯一例外是登记工程主检出上的写与提交**):
|
|
170
|
-
|
|
171
|
-
1. `FEATURE_ALLOW_MAIN=1`(或 60 分钟内创建过的 `/root/features/.allow-main`)→ 放行(人工豁免)
|
|
172
|
-
2. 目标路径已在某个**在用**特性 worktree 内 → 放行(cwd/路径优先,多特性并行互不干扰)
|
|
173
|
-
2b. **命令的有效目标目录**(最后一个 `-C` / `cd`,取不到才退回会话 cwd)落在在用特性 worktree 内 → 放行
|
|
174
|
-
2c. 目标是**已回收特性留下的路标**(原 worktree 路径 → 主检出的符号链接)→ 读放行,写/提交 **`exit 2`**
|
|
175
|
-
阻断,并说明「这是只读门牌、代码已在 main、要改就开新特性」。(2026-10-15 随路标一起加的;
|
|
176
|
-
它不是多余的:规则 2 只认在用特性,路标因此不会走 2 的放行分支。)
|
|
177
|
-
3. 否则,若目标仓库根是**某个特性的主检出**,且本次是「写」(`edit`/`write`)或「会产生 commit 的
|
|
178
|
-
git 子命令」(`commit|merge|rebase|cherry-pick|revert|am`)→ **`exit 2` 阻断**,并分别列出
|
|
179
|
-
**在用**候选 worktree(`*` 标本会话关联、`·` 标 `.active`)与**已回收**特性(路标可读、不是干活的地方)
|
|
180
|
-
4. 其余(非登记工程、只读命令、没有登记特性)→ 放行
|
|
181
|
-
|
|
182
|
-
**判据与 `.active` 无关**:只要一个工程被纳入特性体系,它的主检出就不再是干活的地方;
|
|
183
|
-
**已回收特性也仍然「占着」这条纪律**(它的代码就住在 main 里),所以回收之后再想在主检出上直接
|
|
184
|
-
commit,仍然会被拦——要改就 `feature new` 开新特性。`.active` 只用于「同一工程有多个特性时挑首选」
|
|
185
|
-
与展示。特性 `close`(放弃、删掉注册记录)之后主检出即恢复可写。
|
|
186
|
-
|
|
187
|
-
> **规则 2b 是 2026-10-13 现场补的缺陷修复**:第一版把会话 cwd 也塞进候选集并「任一候选命中主检出
|
|
188
|
-
> 即拦」,于是当会话工作区是主检出时,`cd <worktree> && git commit` 与 `git -C <worktree> commit`
|
|
189
|
-
> 被**误判**成主检出提交(另一个会话实测被拦 3 次,只能靠 `FEATURE_ALLOW_MAIN=1` 绕过)。
|
|
190
|
-
> 正解是**判定命令的有效目标**,而不是「候选里任一命中」——见 §9 的现场记录。
|
|
191
|
-
|
|
192
|
-
### 明确不做的(负向保证)
|
|
193
|
-
|
|
194
|
-
- **不是沙箱**:`FEATURE_ALLOW_MAIN` 与哨兵文件都是**可见、可审计**的逃生门,防的是「工作
|
|
195
|
-
落错地方」这类误操作,不防恶意。哨兵文件默认 60 分钟过期,避免忘了删导致长期失效。
|
|
196
|
-
- **路标只保证读、且只覆盖被跟踪的内容**:它是符号链接,`git` 会解析回主检出,所以对它的写与提交
|
|
197
|
-
被规则 2c 挡住,不会变成绕过制度的口子;worktree 里**被忽略/未跟踪**的临时产物(build 输出等)
|
|
198
|
-
随 `worktree remove` 一起消失——预检只保证「没有未提交的跟踪改动」。`prune` 只 `rm` 符号链接,
|
|
199
|
-
发现目标是真实目录就拒绝。
|
|
200
|
-
- **拦不住 bash 直接写文件**:`echo >`、`sed -i`、heredoc 落盘不在拦截范围(要拦就得解析 shell
|
|
201
|
-
语义,误报代价不可接受)。但那条路产出的改动**仍然会在提交时被规则 3 挡住**,所以
|
|
202
|
-
「任何 commit 只落在特性 worktree」这条硬要求不受影响。
|
|
203
|
-
- **不自动搬移改动**:git 的每个 worktree 有自己的索引与 HEAD,在 A 处暂存的改动**无法**提交到
|
|
204
|
-
B 处。所以设计是「拦住并要求在正确的 worktree 里干活」,而不是「事后把提交改投别处」。
|
|
205
|
-
|
|
206
|
-
## 4. 安装与接线
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
bash /root/plugins/feature-worktrees/install.sh
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
装到(**符号链接**,事实源只有本目录一处):
|
|
213
|
-
`/root/.local/bin/feature`、`/root/.dsh/hooks/{feature,feature-guard,pre-tool,post-edit}*.sh`、
|
|
214
|
-
`/root/.dsh/hooks/dsh-hooks.json`。
|
|
215
|
-
|
|
216
|
-
再让 profile 指向主机级配置(`/root/.dsh/profiles/web/cordis.patch.yml` 的 `hooks-claude-code` 行):
|
|
217
|
-
|
|
218
|
-
```yaml
|
|
219
|
-
configPath: /root/.dsh/hooks/dsh-hooks.json
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
> 为什么把钩子配置从 `/root/hemou/scripts/dsh-hooks.json` 迁到主机级:本能力是**跨工程**的,
|
|
223
|
-
> 主机级行为寄生在某个项目仓库里,那个仓库一移动/清理就全失效。
|
|
224
|
-
|
|
225
|
-
## 5. 钩子链(PreToolUse / PostToolUse)
|
|
226
|
-
|
|
227
|
-
| 时机 | 顺序 | 作用 |
|
|
228
|
-
| --- | --- | --- |
|
|
229
|
-
| `SessionStart` | `session-start.sh` | **首轮之前注入 2–3 行规则**(未关联给「先关联」指引、已关联报出归属、cwd 已在**在用** worktree 内则不打扰;cwd 落在**已回收路标**里或绑定指向已回收特性时,改报「旧路径可读、这里不能干活」)——让会话**主动**关联,而不是被拦出来。只用 `feature_slugs_active`:已回收的不再被报成「在用特性」 |
|
|
230
|
-
| PreToolUse | `feature-guard.sh` → `<repo>/scripts/devloop-hook-commit-guard.sh` | 特性守卫;仓库级提交前守门(hemou 有) |
|
|
231
|
-
| PostToolUse | `<repo>/scripts/devloop-hook-build.sh` → `autosave-hook.sh` | 编辑 `.go` 后 build+vet;语法自洽的改动自动提交并推 gitea |
|
|
232
|
-
|
|
233
|
-
**字段解析纪律**:钩子命令只把 stdin 的 JSON **原样转交**,所有字段一律在各脚本内用 `jq` 取。
|
|
234
|
-
2026-10-11 那版把整段 JSON 当路径传参(`git -C "$p"`、`bash script "$p"`),使三个钩子
|
|
235
|
-
**全部静默失效**——autosave 自 2026-10-12 00:38 起再无记录,`/root/hemou` 里 CLAUDE.md、
|
|
236
|
-
KANBAN.json 一直没被提交。本目录的 `dsh-hooks.json` 顶部的 `_history` 记着这条教训。
|
|
237
|
-
|
|
238
|
-
## 6. autosave 链修复的三个层次(2026-10-13 逐层实测定位)
|
|
239
|
-
|
|
240
|
-
这条链此前**每一层都错**,而每一层都表现为「静默 exit 0」,所以只能靠端到端实测抓:
|
|
241
|
-
|
|
242
|
-
| # | 层次 | 症状 | 修法 |
|
|
243
|
-
| --- | --- | --- | --- |
|
|
244
|
-
| 1 | **入口解析** | 调用方把整段 JSON 当路径传参 → `git -C '<json>'` 必失败 | `autosave-hook.sh` 用 `jq` 解析(兼容 JSON / 目录 / 首行三种输入) |
|
|
245
|
-
| 2 | **仓库归属** | 只看 `.cwd`,而 `cwd` 是**会话工作区**;跨工程编辑时文件在别的工程里 → 无感 | 一律**优先 `.tool_input.file_path`**,其次 `.cwd`;路径逐级上溯到存在的目录(`git -C` 只吃目录) |
|
|
246
|
-
| 3 | **执行目录** | 解析出了仓库根,却没 `cd` 过去 → 引擎用自己的 cwd 找仓库,第一行就退出 | `( cd "$ROOT" && timeout 300 bash "$ENGINE" --push --quiet )` |
|
|
247
|
-
|
|
248
|
-
另外两条**属于平台行为、不是缺陷**,但会让验证产生错觉,必须知道:
|
|
249
|
-
|
|
250
|
-
- **20 秒去抖**:`autosave-unit.sh` 的 `AUTOSAVE_DEBOUNCE=20`(按仓库计,戳在 `<repo>/.devloop/autosave.last`)。
|
|
251
|
-
连续编辑时只有第一次会提交——所以「刚写完没提交」不等于坏了。
|
|
252
|
-
- **`--quiet` 吞输出**:引擎经钩子调用时输出被丢弃,排查要手动 `AUTOSAVE_ENGINE_LOG=1` 跑。
|
|
253
|
-
|
|
254
|
-
## 7. 已知边界
|
|
255
|
-
|
|
256
|
-
- `ls /root/features` 只覆盖「已登记工程」;未登记仓库不受任何约束(故意如此:主机运维、
|
|
257
|
-
临时脚本目录不该被拦)。
|
|
258
|
-
- worktree 是新目录 ⇒ 在 DSH 里是**新的工作区**。要在某特性下开工,请把会话建在该 worktree
|
|
259
|
-
目录(或命令里 `cd` 过去)。
|
|
260
|
-
- **回收不销毁路径,但也不冻结内容**:路标指向的是**活的主检出**,所以旧路径读到的是「现在」的
|
|
261
|
-
代码,不是回收那一刻的快照;主检出后来往前走,旧路径跟着变。要冻结的历史版本只有 git 能给
|
|
262
|
-
(`git -C <主检出> show <mergeCommit>:<路径>`)。
|
|
263
|
-
- 路标是符号链接 ⇒ 用 `rm -rf <路标>/`(**带尾斜杠**)会顺着链接删到主检出里去。清理请走
|
|
264
|
-
`feature prune`(它只 `rm` 链接本身,且发现目标是真实目录就拒绝)。这是刻意接受的残余风险:
|
|
265
|
-
换来的是 0 成本、内容永远正确的旧路径可读性。
|
|
266
|
-
- `/root/plugins`(本目录)被删除会让钩子失效——重跑 `install.sh` 即恢复;`feature doctor` 会报出来。
|
|
267
|
-
- **已知误报**:判定「命令里有没有会产生 commit 的 git 子命令」是文本匹配。已经排除
|
|
268
|
-
「`git` 出现在字符串字面量里」(如 `echo 'git merge main'`)这一类,但把命令当纯文本打印
|
|
269
|
-
且恰好不带引号边界的写法仍可能被误拦。这是刻意选 fail-safe 方向:宁可多拦,不可漏过真正
|
|
270
|
-
落在主检出的提交。真被误拦时用 `FEATURE_ALLOW_MAIN=1` 显式豁免(会留在记录里)。
|
|
271
|
-
- **不自动搬移改动**(同 §3 负向保证):拦的是「在错的地方干活」,不是「事后改投提交」。
|
|
272
|
-
- **文件沙箱在本机是关的**(§2.7):`danger-full-access` 是 worktree 工作流的**必要条件**,不是偏好。
|
|
273
|
-
放开后 DSH 那层 bwrap 围栏不再拦写;制度边界只剩 `feature-guard`(登记工程的主检出与已回收路标)
|
|
274
|
-
与 agent 自身纪律——**没被任何登记工程覆盖的路径(如 `/root/ops`、`/etc`)在会话里是可写的**。
|
|
275
|
-
这是本机为「未值守按需建 worktree」付出的代价;回退方法是把两个生效点改回 `workspace-write`
|
|
276
|
-
(见 §2.7 表),代价是 worktree 又只能靠人批准。
|
|
277
|
-
|
|
278
|
-
## 8. 自测
|
|
279
|
-
|
|
280
|
-
```bash
|
|
281
|
-
bash /root/plugins/feature-worktrees/selftest.sh --live # 114 条:22 沙箱规则 + 4 活体 + 19 生命周期 + 3 首轮注入 + 12 营救/审计/有效目标 + 25 路标/墓碑/prune/relink + 15 预检/ensure + 14 反向
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
自测写成文件而不是一行命令,是因为守卫靠命令文本判定,而测试描述里天然含 `git merge`
|
|
285
|
-
这类词——文件化之后实际执行的命令只剩 `bash selftest.sh`,既避开自触发,也让回归可复跑。
|
|
286
|
-
另一条纪律:自测里 `SID_TEST` 固定并 `export DSH_SESSION_ID` —— `new`/`ensure` 会**自动关联本会话**,
|
|
287
|
-
不固定就会把跑测试的真实会话绑到沙箱特性上(实测让 `doctor` 假失败)。
|
|
288
|
-
|
|
289
|
-
## 9. 现场记录:由真实使用暴露的问题(2026-10-13)
|
|
290
|
-
|
|
291
|
-
另一个会话(`一物万象`,cwd 在主检出)真实使用这套机制时,暴露了两件事;两条都已修并进回归:
|
|
292
|
-
|
|
293
|
-
| # | 现象(现场) | 根因 | 修法 |
|
|
294
|
-
| --- | --- | --- | --- |
|
|
295
|
-
| 1 | 在 worktree 里提交**被守卫拦了 3 次**,最后只能写 `FEATURE_ALLOW_MAIN=1` 绕过 | 第一版对 bash 命令取「候选集」(`-C`/`cd` 目标 **加上会话 cwd**)并**任一命中主检出即拦**;会话工作区正是主检出时,目标其实是 worktree 也照样被拦 | 改为判定**命令的有效目标**(最后一个 `-C`/`cd`;取不到才退回 cwd),并新增规则 2b(有效目标/仓库根本身落在特性 worktree 内即放行) |
|
|
296
|
-
| 2 | 主检出里压着 5 项未提交改动(含 2 篇只此一份的 Agent Note),既改不动也提交不了 | 制度切换前留下的在途改动没有搬运通道 | 新增 `feature rescue`(stash → 在 worktree apply,stash 留备份)与 `feature audit`(只读体检,给出确切回收命令) |
|
|
297
|
-
|
|
298
|
-
**教训(写进纪律)**:钩子的判据必须是「**这次操作实际作用在哪个仓库**」,而不是「相关路径里有没有
|
|
299
|
-
主检出」。前者是事实,后者是启发式——启发式一旦与会话 cwd 耦合,就会把**正确的操作**拦下来,
|
|
300
|
-
而误拦比漏拦更伤:它会训练人去找绕过通道(现场确实发生了)。
|
|
301
|
-
|
|
302
|
-
## 10. 现场记录:回收把会话的引用一起销毁(2026-10-15)
|
|
303
|
-
|
|
304
|
-
用户现场提出:**「一旦 worktree 的代码 merge 到主分支、自己回收了,DSH 会话里所引用的文件就没法访问了。」**
|
|
305
|
-
本机当时可验证:`facet-constitution`、`facet-contract` 两个特性已回收,`feature list --all` 显示
|
|
306
|
-
`[缺失]`——它们的 worktree 目录被 `git worktree remove` 拆掉,任何曾把会话工作区建在那里(或只是
|
|
307
|
-
在对话里 `@` 过某个文件)的会话,路径全部变成死链。这不是「会话坏了」,是**回收的语义错了**:
|
|
308
|
-
它把「合并完成」和「销毁路径」绑在了一起,而路径正是会话与代码之间的唯一纽带。
|
|
309
|
-
|
|
310
|
-
| 决策 | 内容 | 为什么不是别的 |
|
|
311
|
-
| --- | --- | --- |
|
|
312
|
-
| 路标 = 指向主检出的符号链接 | 拆 worktree 后在原路径 `ln -sfn <主检出>` | 0 成本、内容永远等于「代码现在住的地方」;`git` 会把路径解析回主检出,所以既有的规则 3 天然继续兜住写与提交 |
|
|
313
|
-
| 只保证读 | 新增守卫规则 2c:路标上的写/提交一律拦;读放行 | 路标要能读才有用;但让它可写就等于给主检出开了一个看起来像 worktree 的后门 |
|
|
314
|
-
| 记录 A 面 B 面 | `feature.json` 记 `retiredTo/retiredAt` + 特性目录写 `REDIRECT.md` | 注册表事实与磁盘事实都要有,`list/context/doctor/audit` 才不必靠猜路径形状 |
|
|
315
|
-
| 清理与补回各自成命令 | `feature prune`(只删链接)/ `feature relink`(补回) | 清理必须有「不删真实目录」的硬保证;补回让**已经**失联的旧特性(本期两个)也能修,而不必重新回收 |
|
|
316
|
-
| 放弃 = 墓碑 | `feature close` 写 `CLOSED.md`,**不**留路标 | 代码不在 main 里,指向 main 是撒谎;给的是「在哪个分支、怎么读出来」 |
|
|
317
|
-
|
|
318
|
-
**被否掉的方案**:① 回收后重建一个 detached 快照 worktree(`git worktree add --detach <merge-sha>`)
|
|
319
|
-
——路径是真的、`rm -rf` 也伤不到主检出,但每个特性要一份完整检出(hemou 工作区 15G)、内容会与后续
|
|
320
|
-
main 脱节、还要多一套 prune 记账;② 把 `--keep-worktree` 变默认——路径最忠实,可它仍是注册 worktree,
|
|
321
|
-
`list/doctor/audit` 会一直把它当「在用」,且分支合并后就开始漂移;③ 只在会话侧把绑定改指主检出——
|
|
322
|
-
治不了已经写在对话历史里的旧绝对路径。
|
|
323
|
-
|
|
324
|
-
**代价(明写在 §7)**:路标读的是**活的主检出**,不是回收那一刻的快照;并且 `rm -rf <路标>/`
|
|
325
|
-
(带尾斜杠)会顺着符号链接删到主检出——所以清理通道只认 `feature prune`。
|
|
326
|
-
|
|
327
|
-
回归:`selftest.sh` 新增 31 例(路标可读 / 写被拦 / `prune` 不伤主检出 / `relink` 补回 / 墓碑),
|
|
328
|
-
共 99 例全过;本机两个已回收特性用 `feature relink --all --yes` 补回了路标(实测旧路径可读、
|
|
329
|
-
`edit` 被拦)。
|
|
330
|
-
|
|
331
|
-
## 11. 现场记录:按需建 worktree 在未值守时不可用(2026-10-15)
|
|
332
|
-
|
|
333
|
-
用户现场提出(原话):**「一旦会话产生了,需要相应的 worktree 时,它按需去创建对应的 worktree,
|
|
334
|
-
并且这个 worktree 所属的目录应该是具有权限的。所以权限的问题一定要先处理好,而不是等到执行定时任务
|
|
335
|
-
的时候,才发现这个 worktree 还没好需要审批,导致在空闲时候执行待办任务时,还是没法执行。」**
|
|
336
|
-
当天的现场证据是一条两连击的失败链:某会话要建 worktree → 先撞 `/root/features is read-only`
|
|
337
|
-
(文件沙箱拒绝,弹审批)→ 改用 DSH 自带 `create_worktree` → 撞
|
|
338
|
-
`worktree command failed (129): unknown option: --no-lazy-fetch`(本机 git 2.43.0 < 插件要求的 2.45)。
|
|
339
|
-
|
|
340
|
-
| 决策 | 内容 | 为什么不是别的 |
|
|
341
|
-
| --- | --- | --- |
|
|
342
|
-
| 部署默认给足权限 | dsh-web 走 systemd drop-in `Environment=DSH_PERMISSION_MODE=danger-full-access`;nightshift 走 headless profile 里 `sandbox-policy` 的同类覆盖 | 建 worktree 要写会话工作区之外的**两处**,而 `workspace-write` 只有一个可写根(bwrap:`--ro-bind / /` + 仅 `--bind <工作区>`),且策略层没有额外可写路径白名单 ⇒ 无更窄的表达 |
|
|
343
|
-
| 边界交给钩子 | 放开沙箱后,靠 `feature-guard`(主检出写与提交一律拦、已回收路标只读) | 该钩子是主机级、覆盖所有会话且可审计;本机全部会话本来就跑在 root 下,unix 权限不是边界 |
|
|
344
|
-
| 把结论做成体检 | 新增 `feature preflight`:真写特性根、真写 `<repo>/.git`、真跑 `git worktree add --no-checkout`;并入 `feature doctor` | 「权限是否就绪」必须是**可执行、可复跑**的事实,而不是口头保证;未值守会话也要能自己判死 |
|
|
345
|
-
| 幂等入口 | 新增 `feature ensure <特性名> --repos <工程名>`:不在就建、在就核对+关联 | 未值守会话不能提问,确定性的一条命令胜过 `new`/`attach` 之间猜 |
|
|
346
|
-
| 停用 DSH 的 worktree 工具 | web profile patch:`- id: tool-worktree / disabled: true` | 本机必失败(git 版本),且其目录布局绕过 `/root/features` 注册表与守卫;留着只会浪费 schema token 并把会话引向死路 |
|
|
347
|
-
|
|
348
|
-
**被否掉的方案**:① **只给固定跑批会话开权限**——部署默认仍是 `workspace-write`,任何新会话/回落
|
|
349
|
-
会话照样撞墙,属治标;② **宿主机预建 worktree 来回避权限**——预建进程本身仍要写那两处,只是把问题
|
|
350
|
-
挪到另一个进程,而且「按需创建」正是用户要的语义;③ **升级 git 到 2.45 以启用 `create_worktree`**——
|
|
351
|
-
即便可用,它的布局与特性注册表不一致,制度会碎成两套;④ **给沙箱加额外可写路径白名单**——上游
|
|
352
|
-
`dsh-sandbox-policy` 只有 `mode`/`workspaceRoot`,要改 DSH 本体,代价与收益不成比例。
|
|
353
|
-
|
|
354
|
-
**代价(已写进 §7)**:本机 DSH 的 bwrap 围栏实际关闭,未被登记工程覆盖的路径(`/root/ops`、`/etc` 等)
|
|
355
|
-
在会话里亦可写;回退路径是改回 `workspace-write`(代价:worktree 只能靠人批准)。
|
|
356
|
-
|
|
357
|
-
回归:`selftest.sh` 新增 15 例(`preflight` 通过/按工程跑/不存在工程失败/不留残留、`doctor` 含体检且
|
|
358
|
-
全绿、`ensure` 幂等/新建/自动关联/无 `--repos` 拒绝/已回收拒绝),并把 `SID_TEST` 固定导出以消除
|
|
359
|
-
「自测把真会话绑到沙箱特性」的假失败;共 **114 例全过**。端到端另验:真实 `dsh --profile headless`
|
|
360
|
-
worker(nightshift 用的就是它)在无人值守条件下跑 `feature preflight` 全绿;`bwrap` 复刻实验证明
|
|
361
|
-
`workspace-write` 下特性根与 git 元数据均不可写。
|