@minonl/dia 0.0.9 → 0.0.11

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/docs/index.md CHANGED
@@ -31,7 +31,7 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
31
31
  | [价目表(tokens → 金额)](pricing.md) | 原则:没有价格就不编造;unpriced 明确记录,预算按 tokens 兜底。 |
32
32
  | [目标提案(team next)](proposals.md) | 定位:团队可以提候选,不能定目标。 |
33
33
  | [Sandbox 细化设计(dia)](security-sandbox.md) | 依据:dsh 本机源码实测、Hermes/Pi 取证、[/tmp/dia-sbx 实验]。 |
34
- | [服务(service):可托管的执行体](service-noun.md) | 状态:提案(设计,未实现)。只做脚本型服务;healthcheck 按专用退出码域判定,旧结果超新鲜度窗口即报 stale。 |
34
+ | [服务(service):可托管的执行体](service-noun.md) | 状态:已实现(v0.0.10,脚本型 + agent 型)。脚本型走 entry.cmd,agent 型走 entry.agent + healthcheck.kind="acp"; |
35
35
  | [dia 状态页(单一事实源)](status.md) | 任何"现在做到哪了"的问题,以本页为准;其他文档是细节。 |
36
36
  | [全自动团队:人只在验收介入](team-automation.md) | 目标:计划→实现→验证→对抗→修复全自动循环;人只在验收阶段拍板。 |
37
37
  | [团队章程:谁来做 dia(开发团队)](team.md) | 2026-10-28 升级:开发团队执行模式改为全自动循环(计划→实现→验证→对抗→修复),人只在验收阶段介入; |
@@ -78,3 +78,27 @@ dia graph --tui --no-color # 关 ANSI 颜色
78
78
  - 按状态分组显示 task → deps / touches / blocked_by;claims 单列;
79
79
  - 不启 HTTP/daemon,遵守框架“无网络模块”约束;
80
80
  - `--task t-...` 只显示该任务的直接邻居。
81
+
82
+ ---
83
+
84
+ # handoff + claim 联动(M2)
85
+
86
+ ```bash
87
+ dia claim src/collab.ts --reason "正在重构"
88
+ dia out --to ses_xxx --kind handoff --subject "交接" --body-file handoff.md \
89
+ --touches src/collab.ts,docs/msg-collab-m1.md
90
+ ```
91
+
92
+ - `--touches a,b`(也接受重复 `--touch`)写入消息 `refs.touches`;
93
+ - 发送时如果有他人活跃 claim,输出 `⚠ claim overlap`(advisory,仍发送);
94
+ - 收件方 `dia in` 会在消息下显示 `⚠ claim: <path> 由 <holder> 持有`;
95
+ - 不自动排队/阻断;claim 仍是可见性工具。
96
+
97
+ ## 清理历史(可选)
98
+
99
+ ```bash
100
+ dia claims --gc --days 7
101
+ ```
102
+
103
+ - 保留活跃 claim + 最近 7 天的记录,删除更早的已过期/已释放事件;
104
+ - 只压缩本地 `claims.jsonl`,不影响任何活跃 claim。
package/docs/overview.md CHANGED
@@ -85,6 +85,12 @@ $ dia --persona yan kb scene S1 → 正常出场景卡
85
85
 
86
86
  ---
87
87
 
88
+ ## 3.1 CI 口径(见 README「CI 矩阵」)
89
+
90
+ - required = `test (ubuntu-latest)` + `test (macos-latest)`(分支保护,`enforce_admins`)
91
+ - `windows-smoke` 非阻塞:Windows 后端未实现(`dia job install` rc=3),落地后再升 required
92
+ - **docs-only 快通道**:只改 `docs/**` / 根 `*.md` / `llms*.txt` 时跳过测试矩阵
93
+
88
94
  ## 4. 已知缺口(按优先级)
89
95
 
90
96
  1. **yan 安装落后来源仓 1 个提交**(`83fbaa9` vs `8d10cc5`,内容是 dsh profile 根 `cordis.yml` 修复)。
@@ -1,8 +1,11 @@
1
1
  # 服务(service):可托管的执行体
2
2
 
3
- > 状态:提案(设计,未实现)。只做脚本型服务;healthcheck 按专用退出码域判定,旧结果超新鲜度窗口即报 stale。
3
+ > 状态:**已实现(v0.0.10,脚本型 + agent 型)**。脚本型走 `entry.cmd`,agent 型走 `entry.agent` + `healthcheck.kind="acp"`;
4
+ > healthcheck 按专用码域 `0/20/21` 判定(外加 `124`=timeout、信号=crash),旧结果超新鲜度窗口即报 `unknown (stale …)`。
5
+ > **实现状态逐节对照见 §15。**
4
6
  > 日期 2026-11-05(rev.2:按 [设计评审 B1–B5 / C1–C7 / D1–D4](review/service-noun-review-2026-11-05.md) 改稿;
5
- > **rev.3**:按复核意见改「去抖单一写入方 / health 码域 `0/20/21` / timeout 映射 / 手动检查落台账 / adopted 口径」,见 §13)。
7
+ > **rev.3**:按复核意见改「去抖单一写入方 / health 码域 `0/20/21` / timeout 映射 / 手动检查落台账 / adopted 口径」,见 §13;
8
+ > **rev.4**:回填实现状态(PR #49 / #54 / #60)与 §15 对照表)。
6
9
  > 关联:[`cli.md`](cli.md)(名词与动词矩阵 / 退出码)· [`persona-install.md`](persona-install.md)(`dia.json` schema)·
7
10
  > [`kit-install.md`](kit-install.md) · [`acp-integration.md`](acp-integration.md)(agent 会话型服务)· `packages/job`(生命周期、状态台账)
8
11
 
@@ -243,9 +246,11 @@ unknown(stale)**不单独返回**——`health` 是实时执行,不存在
243
246
 
244
247
  - `job` 仍是**唯一调度实现**(不新增第二套);健康检查任务本身就是一个 `job`(`periodic`)。
245
248
  - `service` 只多一层「声明 + 校验 + 健康检查」。
246
- - `dia job status` 的每行增加 `health=<ok|degraded|fail|unknown|n/a> (Ns ago)`(读 §4.5 缓存,不执行)。
249
+ - `dia job status` 的每行增加 `health=<ok|degraded|fail|unknown|n/a> (Ns ago)`(读 §4.5 缓存,不执行)。**✅ 已实现(PR #54)**:
250
+ 仅对 `local.dia-job-svc-<id>` 行追加,其它行原样(向后兼容)。
247
251
  - 对 `service` 型(常驻)任务:Linux 走 systemd `--user`,**常驻需 `loginctl enable-linger`**(`job doctor` 已提示,`dia service add` 一并转述);
248
252
  对 `periodic` 任务,cron 后端**没有进程状态**,health 是唯一判据 → 新鲜度条款(§4.6)尤其关键。
253
+ - **性能**:只读台账、零执行;实测 N=50 时 `3.1ms`(阈值 200ms),基准见 `tests/service-health-status.perf.test.ts`。
249
254
 
250
255
  ## 7. 边界与异常
251
256
 
@@ -355,7 +360,8 @@ unknown(stale)**不单独返回**——`health` 是实时执行,不存在
355
360
 
356
361
  ## 14. P2:agent 型服务(entry = persona 会话)
357
362
 
358
- > 状态:**草案(待实现)**。§0 已定「本版只做脚本型」;本节把 P2 的形状先定下来,避免实现时二次发明语义。
363
+ > 状态:**已实现(PR #60)**。本节定义脚本型之外的 agent 型入口(`entry.agent` + `healthcheck.kind="acp"`);
364
+ > 实现与本节一致,差异见 §15。
359
365
 
360
366
  ### 14.1 与脚本型的关系
361
367
 
@@ -428,3 +434,31 @@ flowchart LR
428
434
  - [ ] 单测:`runAcpHealth` 用 `tests/fixtures/fake-acp.mjs` 覆盖 `initialize` 失败 / `session/new` 失败 / 正常三态
429
435
  - [ ] 单测:`healthcheck.kind` 缺省时按脚本型(向后兼容)
430
436
  - [ ] e2e:装一个 agent 型 demo(fake ACP)→ `dia job status` 显示 `health=ok…`,杀掉端点后变 `degraded(stale)`
437
+
438
+ ## 15. 实现状态(2026-11-06 回填)
439
+
440
+ > 规则:**设计先改、代码同批**(R35)。本节是「设计 ↔ 实现」的对照表;后续改动必须同批更新本表。
441
+
442
+ | 设计小节 | 状态 | 实现落点 |
443
+ |---|---|---|
444
+ | §2 构成(entry/lifecycle/contract/healthcheck) | ✅ | `src/service.ts`:`ServiceManifest` / `validateServiceManifest`(PR #49) |
445
+ | §3 清单与校验(entry 规则、`E_MANIFEST`) | ✅ | `checkEntry` / `checkAgentEntry` / `resolveEntry`(#49、#60) |
446
+ | §4.1 码域 `0/20/21` + `124` + 信号 | ✅ | `judgeHealth` / `judgeSpawnResult`;`124` 与 `137/143` 分流(#49;无 `timeout(1)` 的兜底见 #49 后续修复 `29a4341`) |
447
+ | §4.2 执行环境 / `E_BACKEND` | ✅ | `healthShell`、`checkCronInterval`(#49) |
448
+ | §4.3 去抖 + 单一写入方 + `gen` | ✅ | `transition`(`gen` 不同的行不计数)(#49) |
449
+ | §4.4 崩溃语义(保持上次状态 + `last_error`) | ✅ | `judgeSpawnResult` + `transition`(#49) |
450
+ | §4.5 台账(`state.jsonl` + `kind:"health"`) | ✅ | `appendHealthRecord` / `findLastHealth` / `readHealthLines`(#49、#54) |
451
+ | §4.6 新鲜度(`interval × 1.5` → stale) | ✅ | `healthView` / `staleAfter`(#54) |
452
+ | §5 CLI(add/remove/list/show/verify/health) | ✅ | `packages/cli/src/index.ts`(#49);`health` 的 agent 分支(#60) |
453
+ | §6 `dia job status` 追列 health | ✅ | `decorateStatus` + `runJobCapture`(#54) |
454
+ | §7 边界(`E_UNSTABLE_ENTRY` / adopted / three-state) | ✅ | `verify` / `healthView`(#49、#54) |
455
+ | §14 agent 型(`entry.agent` + `kind:"acp"`) | ✅ | `acpAgentCommand` / `runAcpHealth` / `recordHealth`(#60) |
456
+ | §6 性能基准(N=50 ≤200ms) | ✅ | 实测 **3.1ms**(单服务 0.1ms,macOS / bun 1.4.4);`tests/service-health-status.perf.test.ts` 断言 ≤200ms 且不随 N 线性增长 |
457
+
458
+ ### 15.1 已知与设计的偏差
459
+
460
+ | 项 | 设计 | 实现 | 处置 |
461
+ |---|---|---|---|
462
+ | `healthcheck.kind` 缺省 | 未写 | 缺省 `script`(向后兼容) | 与 §14.6 一致,已写回 §14 |
463
+ | `dia service health` 的 agent 分支 | §5 未细化 | 先跑 ACP 探测再落 `source:"manual"` 行 | 与 §5/§14 语义一致,无需改设计 |
464
+ | `entry.agent` 与 `healthcheck.kind` 的强绑定 | §14.6 验收项 | 已实现为 `E_MANIFEST`(agent 型必须 acp;acp 只能配 agent 型) | 已写回 §14.2 校验说明 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minonl/dia",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "description": "dia (嗲) — lightweight agent framework: knowledge index, audit, session ingest",
6
6
  "bin": {
@@ -1263,15 +1263,32 @@ exit 0
1263
1263
  HOOKBODY
1264
1264
  }
1265
1265
 
1266
- wt_global_hooks_dir() { printf '%s/hooks' "$(dia_wt_data_root 2>/dev/null || echo "${DIA_ROOT:-$HOME/.dia}")"; }
1267
- wt_write_global_hooks() {
1268
- local dir; dir="$(wt_global_hooks_dir)"
1269
- mkdir -p "$dir"
1270
- local h
1271
- for h in pre-commit pre-push post-checkout; do
1272
- { printf '#!/usr/bin/env bash\n# dia-wt guard(本机全局钩子;由 `dia wt adopt` 安装,勿手改)\n'; wt_guard_body; } > "$dir/$h"
1273
- chmod +x "$dir/$h"
1274
- done
1266
+ # simple-git-hooks RC 注入目录(跟随 dia 数据根)
1267
+ wt_hooks_rc_dir() { printf '%s/hooks' "${DIA_ROOT:-${DIA_DATA_DIR:-$HOME/.dia}}"; }
1268
+ # 在 ~/.zshenv 里确保 export KEY=VALUE(幂等;带 marker 便于移除)
1269
+ wt_ensure_env_export() {
1270
+ local key="$1" val="$2" rc="${DIA_WT_SHELL_RC:-$HOME/.zshenv}"
1271
+ local marker="# dia-wt: $key"
1272
+ [ -f "$rc" ] || : > "$rc"
1273
+ grep -q "^$marker" "$rc" 2>/dev/null && return 0
1274
+ printf '\n%s\nexport %s="%s"\n' "$marker" "$key" "$val" >> "$rc"
1275
+ }
1276
+ wt_write_hook() {
1277
+ local hooksdir="$1" name="$2" extra="${3:-}" chain="${4:-}"
1278
+ {
1279
+ printf '#!/usr/bin/env bash\n# dia-wt guard(managed by `dia wt adopt`;勿手改)\n'
1280
+ wt_guard_body
1281
+ } > "$hooksdir/$name"
1282
+ if [ -n "$chain" ]; then
1283
+ python3 - "$hooksdir/$name" "$chain" <<'PYCHAIN'
1284
+ import sys
1285
+ p, chain = sys.argv[1], sys.argv[2]
1286
+ s = open(p).read()
1287
+ s = s.replace('orig="$common/hooks/$name"', 'orig="%s/$name"' % chain.replace('\\', '\\\\').replace('"', '\\"'))
1288
+ open(p, "w").write(s)
1289
+ PYCHAIN
1290
+ fi
1291
+ chmod +x "$hooksdir/$name"
1275
1292
  }
1276
1293
 
1277
1294
  cmd_guard() {
@@ -1329,11 +1346,13 @@ cmd_adopt() {
1329
1346
 
1330
1347
  if [ "$mode" = "revert" ]; then
1331
1348
  [ -f "$cfg" ] || die "未 adopt(缺 $cfg)"
1332
- local saved
1349
+ local saved mgr
1333
1350
  saved="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg" 2>/dev/null || true)"
1334
- # 只还原 adopt 之前就存在的仓库级 hooksPath;当前若被别人改过则不动
1335
- if [ -n "$saved" ] && [ "$prev" = "$hooksdir" ]; then
1336
- git -C "$repo" config --local core.hooksPath "$saved"
1351
+ mgr="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
1352
+ if [ "$mgr" = "local" ] || [ -z "$mgr" ]; then
1353
+ if [ "$prev" = "$hooksdir" ]; then
1354
+ if [ -n "$saved" ]; then git -C "$repo" config --local core.hooksPath "$saved"; else git -C "$repo" config --local --unset core.hooksPath 2>/dev/null || true; fi
1355
+ fi
1337
1356
  fi
1338
1357
  rm -rf "$cfgdir"
1339
1358
  echo "reverted:$cfgdir"
@@ -1348,16 +1367,31 @@ cmd_adopt() {
1348
1367
  local cfgDef; cfgDef="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
1349
1368
  [ -n "$cfgDef" ] && def="$cfgDef"
1350
1369
  fi
1351
- local ghd ghp
1352
- ghd="$(wt_global_hooks_dir)"
1353
- ghp="$(git config --global --get core.hooksPath 2>/dev/null || true)"
1354
- [ "$ghp" = "$ghd" ] || { echo "FAIL D02 全局 core.hooksPath 未指向 $ghd(当前:${ghp:-未设置})"; bad=1; }
1355
- for h in pre-commit pre-push post-checkout; do
1356
- [ -x "$ghd/$h" ] || { echo "FAIL D02 缺全局钩子 $h"; bad=1; }
1357
- done
1358
- if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
1359
- ( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$ghd/pre-commit" >/dev/null 2>&1 ) \
1360
- && { echo "FAIL D03 守卫探针未拦截 $def 提交"; bad=1; }
1370
+ local mgr2; mgr2="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
1371
+ if [ "$mgr2" = "external" ]; then
1372
+ echo "WARN D02 $repo 由其它 hook manager 接管(core.hooksPath=$prev),守卫未生效;建议改用 simple-git-hooks"
1373
+ elif [ "$mgr2" = "simple-git-hooks" ]; then
1374
+ local rcf; rcf="$(wt_hooks_rc_dir)/simple-git-hooks-rc.sh"
1375
+ [ -f "$rcf" ] || { echo "FAIL D02 缺 simple-git-hooks RC:$rcf"; bad=1; }
1376
+ grep -rqs "SIMPLE_GIT_HOOKS_RC" "$HOME/.zshenv" || { echo "FAIL D02 shell 未 export SIMPLE_GIT_HOOKS_RC"; bad=1; }
1377
+ python3 - "$repo/package.json" <<'PYM' >/dev/null 2>&1 || { echo "FAIL D02 package.json 未启用 simple-git-hooks"; bad=1; }
1378
+ import json,sys
1379
+ d=json.load(open(sys.argv[1])); c=d.get("simple-git-hooks"); dev=d.get("devDependencies",{}) or {}
1380
+ raise SystemExit(0 if isinstance(c,dict) or "simple-git-hooks" in dev else 1)
1381
+ PYM
1382
+ if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
1383
+ ( cd "$repo" && DIA_WT_GUARD_PROBE=1 bash "$SELF" guard pre-commit >/dev/null 2>&1 ) \
1384
+ && { echo "FAIL D03 守卫探针未拦截 $def 提交"; bad=1; }
1385
+ fi
1386
+ else
1387
+ case "$prev" in /*) ;; *) echo "FAIL D02 本地 core.hooksPath 非绝对($prev)"; bad=1 ;; esac
1388
+ for h in pre-commit pre-push post-checkout; do
1389
+ [ -x "$hooksdir/$h" ] || { echo "FAIL D02 缺 $h 钩子"; bad=1; }
1390
+ done
1391
+ if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
1392
+ ( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
1393
+ && { echo "FAIL D03 守卫探针未拦截 $def 提交"; bad=1; }
1394
+ fi
1361
1395
  fi
1362
1396
  if [ "$json" = 1 ]; then
1363
1397
  python3 -c 'import json,sys;print(json.dumps({"repo":sys.argv[1],"ok":sys.argv[2]=="0","defaultBranch":sys.argv[3]},ensure_ascii=False,separators=(",",":")))' "$repo" "$bad" "$def"
@@ -1383,34 +1417,78 @@ cmd_adopt() {
1383
1417
  [ -n "$def" ] || die "默认分支不可解析;请 --default-branch main"
1384
1418
  mkdir -p "$hooksdir"
1385
1419
  # 安装/刷新本机全局钩子,并让本仓走全局钩子(不改 package.json、不占用仓库钩子目录)
1386
- local ghd ghp
1387
- ghd="$(wt_global_hooks_dir)"
1388
- ghp="$(git config --global --get core.hooksPath 2>/dev/null || true)"
1389
- # 配置外溢防线:全局 hooksPath 若已被别人设置,绝不覆盖
1390
- if [ -n "$ghp" ] && [ "$ghp" != "$ghd" ]; then
1391
- die "全局 core.hooksPath 已指向 $ghp(非 dia),拒绝覆盖;请先自行处理该配置"
1392
- fi
1393
- wt_write_global_hooks
1394
- git config --global core.hooksPath "$ghd"
1395
- # 仓库级 hooksPath:旧版 dia 目录 → 撤掉迁移;别人的管理器(husky/.githooks)→ 保留并提示
1396
- if [ -n "$prev" ] && [ "$prev" != "$ghd" ]; then
1397
- if [ "$prev" = "$hooksdir" ]; then
1398
- git -C "$repo" config --local --unset core.hooksPath 2>/dev/null || true; prev=""
1399
- else
1400
- echo "[dia-wt] ⚠️ 该仓本地 core.hooksPath=$prev(由 hook manager 接管),全局守卫对本仓不生效" >&2
1401
- fi
1420
+ # ── 方案:每个仓本地钩子;simple-git-hooks 仓改用它的 RC 注入 ──────────────
1421
+ # 不再写任何全局 git 配置(避免外溢)。
1422
+ if [ -f "$repo/package.json" ] && python3 - "$repo/package.json" <<'PYCHK'
1423
+ import json,sys
1424
+ d=json.load(open(sys.argv[1]))
1425
+ cfg=d.get("simple-git-hooks")
1426
+ dev=d.get("devDependencies",{}) or {}
1427
+ raise SystemExit(0 if isinstance(cfg,dict) or "simple-git-hooks" in dev else 1)
1428
+ PYCHK
1429
+ then
1430
+ # simple-git-hooks:钩子由管理器生成到 .git/hooks,我们不碰;通过 SIMPLE_GIT_HOOKS_RC 注入守卫
1431
+ local rcdir rcf
1432
+ rcdir="$(wt_hooks_rc_dir)"; rcf="$rcdir/simple-git-hooks-rc.sh"
1433
+ mkdir -p "$rcdir"
1434
+ cat > "$rcf" <<'RC'
1435
+ # 由 simple-git-hooks 生成的钩子 source 本文件(SIMPLE_GIT_HOOKS_RC)
1436
+ # 只对已 adopt 的仓生效;未 adopt → 直接返回
1437
+ if command -v dia >/dev/null 2>&1; then
1438
+ dia wt guard "$(basename "$0")" || exit $?
1439
+ fi
1440
+ RC
1441
+ wt_ensure_env_export SIMPLE_GIT_HOOKS_RC "$rcf"
1442
+ python3 - "$cfg" "$repo" "$def" "$template" "$prev" <<'PYG'
1443
+ import json, sys, datetime
1444
+ cfg, repo, br, tmpl, prev = sys.argv[1:6]
1445
+ json.dump({
1446
+ "schema": 2, "repo": repo, "defaultBranch": br, "template": tmpl,
1447
+ "hooksManager": "simple-git-hooks", "hooksPrevious": prev or None,
1448
+ "adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
1449
+ }, open(cfg, "w"), ensure_ascii=False, indent=1)
1450
+ PYG
1451
+ echo "adopted:$repo(hook manager = simple-git-hooks;守卫经 SIMPLE_GIT_HOOKS_RC 注入)"
1452
+ return 0
1402
1453
  fi
1403
- rm -rf "$hooksdir" 2>/dev/null || true
1404
- python3 - "$cfg" "$repo" "$def" "$template" "$prev" <<'PYG'
1454
+
1455
+ # 其它仓:自己管 <common>/dia/hooks,并把本地 core.hooksPath 指过去。
1456
+ # 若该仓已有**别人**的本地 hooksPath(husky/.githooks 等),不抢占:只报告,交由转为 simple-git-hooks 处理。
1457
+ if [ -n "$prev" ] && [ "$prev" != "$hooksdir" ]; then
1458
+ python3 - "$cfg" "$repo" "$def" "$template" "$prev" <<'PYG'
1405
1459
  import json, sys, datetime
1406
1460
  cfg, repo, br, tmpl, prev = sys.argv[1:6]
1407
1461
  json.dump({
1408
1462
  "schema": 2, "repo": repo, "defaultBranch": br, "template": tmpl,
1409
- "hooksManager": "global", "hooksPrevious": prev or None,
1463
+ "hooksManager": "external", "hooksPrevious": prev,
1464
+ "adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
1465
+ }, open(cfg, "w"), ensure_ascii=False, indent=1)
1466
+ PYG
1467
+ echo "[dia-wt] ⚠️ $repo 的 core.hooksPath=$prev 由其它管理器管理,未接管(守卫不生效);建议改用 simple-git-hooks(见 docs/worktree.md)" >&2
1468
+ return 0
1469
+ fi
1470
+ mkdir -p "$hooksdir"
1471
+ local savedPrev=""
1472
+ [ -f "$cfg" ] && savedPrev="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg" 2>/dev/null || true)"
1473
+ [ "$prev" = "$hooksdir" ] && prev="$savedPrev"
1474
+ local chain="$prev"
1475
+ [ -n "$chain" ] || chain="$common/hooks"
1476
+ [ -f "$cfg" ] && cp "$cfg" "$cfgdir/config.json.bak.$(date +%s)"
1477
+ wt_write_hook "$hooksdir" pre-commit "$def" "$chain"
1478
+ wt_write_hook "$hooksdir" pre-push "$def" "$chain"
1479
+ wt_write_hook "$hooksdir" post-checkout "$def" "$chain"
1480
+ git -C "$repo" config --local core.hooksPath "$hooksdir"
1481
+ python3 - "$cfg" "$repo" "$def" "$template" "$prev" "$chain" <<'PYG'
1482
+ import json, sys, datetime
1483
+ cfg, repo, br, tmpl, prev, chain = sys.argv[1:7]
1484
+ json.dump({
1485
+ "schema": 2, "repo": repo, "defaultBranch": br, "template": tmpl,
1486
+ "hooksManager": "local", "hooksPath": cfg.rsplit("/",1)[0] + "/hooks",
1487
+ "hooksPrevious": prev or None, "hooksChain": chain,
1410
1488
  "adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
1411
1489
  }, open(cfg, "w"), ensure_ascii=False, indent=1)
1412
1490
  PYG
1413
- echo "adopted:$repo(守卫 = 本机全局钩子 $(wt_global_hooks_dir))"
1491
+ echo "adopted:$repo(守卫 = 仓库本地钩子 $hooksdir)"
1414
1492
  return 0
1415
1493
  # 保留首次 adopt 记下的 hooksPrevious(重复 apply 时 prev 已是我们自己的 hooksdir)
1416
1494
  local savedPrev=""
@@ -1467,13 +1545,17 @@ cmd_doctor() {
1467
1545
  fi
1468
1546
  [ -n "$def" ] || def="$(wt_default_branch "$repo")"
1469
1547
  # --fix:无损修复(重写三个守卫钩子 + hooksPath 绝对路径)。只对已 adopt 的仓生效。
1470
- if [ "$fix" = 1 ] && [ -f "$cfg" ]; then
1471
- wt_write_global_hooks
1472
- git config --global core.hooksPath "$(wt_global_hooks_dir)"
1473
- [ "$json" = 0 ] && echo "FIX $repo 全局钩子已重装"
1474
- fi
1475
- if [ "$fix" = 1 ] && [ -f "$cfg" ] && [ "$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)" = "global" ]; then
1476
- :
1548
+ if [ "$fix" = 1 ] && [ -f "$cfg" ] && [ "$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)" = "simple-git-hooks" ]; then
1549
+ local rcdir rcf; rcdir="$(wt_hooks_rc_dir)"; rcf="$rcdir/simple-git-hooks-rc.sh"
1550
+ mkdir -p "$rcdir"
1551
+ cat > "$rcf" <<'RC'
1552
+ # 由 simple-git-hooks 生成的钩子 source 本文件(SIMPLE_GIT_HOOKS_RC)
1553
+ if command -v dia >/dev/null 2>&1; then
1554
+ dia wt guard "$(basename "$0")" || exit $?
1555
+ fi
1556
+ RC
1557
+ wt_ensure_env_export SIMPLE_GIT_HOOKS_RC "$rcf"
1558
+ [ "$json" = 0 ] && echo "FIX $repo simple-git-hooks RC 已重建"
1477
1559
  elif [ "$fix" = 1 ] && [ -f "$cfg" ]; then
1478
1560
  local fixChain=""
1479
1561
  fixChain="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksChain") or "")' "$cfg" 2>/dev/null || true)"
@@ -1489,25 +1571,33 @@ cmd_doctor() {
1489
1571
  local bad=0 mcfg=""
1490
1572
  [ -f "$cfg" ] || { echo "FAIL D01 $repo 未 adopt"; bad=1; }
1491
1573
  [ -f "$cfg" ] && mcfg="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
1492
- local ghd ghp
1493
- ghd="$(wt_global_hooks_dir)"
1574
+ local ghp mgr2
1494
1575
  ghp="$(git config --global --get core.hooksPath 2>/dev/null || true)"
1495
- # D20 全局配置归属:只认 dia 自己的 hooksPath,被别人占用就报出来(绝不自动覆盖)
1496
- [ "$ghp" = "$ghd" ] || { echo "FAIL D20 全局 core.hooksPath 未指向 $ghd(当前:${ghp:-未设置})"; bad=1; }
1497
- for h in pre-commit pre-push post-checkout; do
1498
- [ -x "$ghd/$h" ] || { echo "FAIL D02 缺全局钩子 $h"; bad=1; }
1499
- done
1500
- # D21 仓库级 hooksPath 外溢:本地设置会覆盖全局(husky/.githooks 等)
1501
- if [ -n "$prev" ]; then
1502
- if [ "$prev" = "$hooksdir" ]; then
1503
- echo "FAIL D21 $repo 旧版本地 hooksPath 残留($prev);跑 dia wt adopt --apply 迁移到全局钩子"; bad=1
1504
- else
1505
- echo "WARN D21 $repo 本地 hooksPath=$prev 覆盖全局钩子,本仓守卫不生效"
1506
- fi
1576
+ # D20 全局配置残留:旧版把守卫装到全局 hooksPath;现在改为每仓本地,必须清掉
1577
+ if [ -n "$ghp" ] && [ -f "$ghp/pre-commit" ] && grep -q "dia-wt guard" "$ghp/pre-commit" 2>/dev/null; then
1578
+ echo "FAIL D20 全局 core.hooksPath 仍指向 dia 旧钩子目录($ghp);执行 git config --global --unset core.hooksPath"
1579
+ bad=1
1507
1580
  fi
1508
- if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
1509
- ( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$ghd/pre-commit" >/dev/null 2>&1 ) \
1510
- && { echo "FAIL D03 $repo 守卫探针未拦截 $def 提交"; bad=1; }
1581
+ mgr2="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
1582
+ if [ "$mgr2" = "external" ]; then
1583
+ echo "WARN D02 $repo 由其它 hook manager 接管(core.hooksPath=$prev),守卫未生效;建议改用 simple-git-hooks"
1584
+ elif [ "$mgr2" = "simple-git-hooks" ]; then
1585
+ local rcf; rcf="$(wt_hooks_rc_dir)/simple-git-hooks-rc.sh"
1586
+ [ -f "$rcf" ] || { echo "FAIL D02 $repo 缺 simple-git-hooks RC:$rcf"; bad=1; }
1587
+ grep -rqs "SIMPLE_GIT_HOOKS_RC" "$HOME/.zshenv" || echo "WARN D02 $repo shell 未 export SIMPLE_GIT_HOOKS_RC(守卫不会执行)"
1588
+ if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
1589
+ ( cd "$repo" && DIA_WT_GUARD_PROBE=1 bash "$SELF" guard pre-commit >/dev/null 2>&1 ) \
1590
+ && { echo "FAIL D03 $repo 守卫探针未拦截 $def 提交"; bad=1; }
1591
+ fi
1592
+ else
1593
+ case "$prev" in /*) ;; *) echo "FAIL D02 $repo 本地 core.hooksPath 非绝对($prev)"; bad=1 ;; esac
1594
+ for h in pre-commit pre-push post-checkout; do
1595
+ [ -x "$hooksdir/$h" ] || { echo "FAIL D02 $repo 缺 $h"; bad=1; }
1596
+ done
1597
+ if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
1598
+ ( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
1599
+ && { echo "FAIL D03 $repo 守卫未拦截 $def 提交"; bad=1; }
1600
+ fi
1511
1601
  fi
1512
1602
  if wt_is_main_checkout "$repo"; then
1513
1603
  [ -z "$(git -C "$repo" status --porcelain 2>/dev/null)" ] || echo "WARN D04 $repo 主 checkout 有未提交改动"
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # adopt / doctor / gate 回归测试(2026-11-06:本机全局钩子模型)
3
- # 覆盖:dry-run、apply(全局钩子)、check/doctor、主 checkout 守卫、链式调用仓库钩子、
4
- # 非 adopt 仓透传、敏感扫描、adopt --all、gate、revert。
2
+ # adopt / doctor / gate 回归测试(2026-11-06:每仓本地钩子 + simple-git-hooks RC)
5
3
  set -uo pipefail
6
4
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
5
  WT="$HERE/../bin/dia-wt"
@@ -9,29 +7,29 @@ TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
9
7
  PASS=0; FAIL=0
10
8
  ok(){ echo " ✓ $1"; PASS=$((PASS+1)); }; bad(){ echo " ✗ $1"; FAIL=$((FAIL+1)); }
11
9
 
12
- # 隔离:HOME / 全局 git 配置 / 数据根,避免污染真实机器
10
+ # 隔离 HOME / 全局 git 配置 / 数据根 / shell rc(避免污染真实机器)
13
11
  export HOME="$TD/home"; mkdir -p "$HOME"
14
12
  export GIT_CONFIG_GLOBAL="$TD/gitconfig"; : > "$GIT_CONFIG_GLOBAL"
15
13
  export GIT_CONFIG_NOSYSTEM=1
16
- export DIA_WT_DATA_ROOT="$TD/data"
17
- GH="$TD/data/hooks"
14
+ export DIA_ROOT="$TD/dia"
15
+ export DIA_WT_SHELL_RC="$HOME/.zshenv"
18
16
 
19
- R="$TD/repo"; mkdir -p "$R"; git -C "$R" init -q -b main
20
- git -C "$R" config user.email t@example.com; git -C "$R" config user.name t
21
- echo a > "$R/a.txt"; git -C "$R" add -A; git -C "$R" commit -qm init
17
+ mk_repo(){ local r="$1"; mkdir -p "$r"; git -C "$r" init -q -b main; git -C "$r" config user.email t@example.com; git -C "$r" config user.name t; echo a > "$r/a.txt"; git -C "$r" add -A; git -C "$r" commit -qm init; }
22
18
 
19
+ R="$TD/repo"; mk_repo "$R"
23
20
  out="$(bash "$WT" adopt "$R" --json)"
24
21
  printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "dry-run 识别默认分支" || bad "dry-run: $out"
25
22
  [ ! -f "$R/.git/dia/config.json" ] && ok "dry-run 不落盘" || bad "dry-run 落盘了"
26
23
 
27
24
  bash "$WT" adopt "$R" --apply >/dev/null 2>&1 || bad "apply 失败"
28
25
  [ -f "$R/.git/dia/config.json" ] && ok "config 已写" || bad "缺 config"
29
- [ "$(git config --global --get core.hooksPath)" = "$GH" ] && ok "全局 hooksPath 指向 $GH" || bad "全局 hooksPath 未设置"
30
- for h in pre-commit pre-push post-checkout; do [ -x "$GH/$h" ] || bad "缺全局钩子 $h"; done
31
- ok "全局三钩子可执行"
32
- [ -z "$(git -C "$R" config --local --get core.hooksPath || true)" ] && ok "不再设置仓库本地 hooksPath" || bad "仍设了本地 hooksPath"
33
- python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));sys.exit(0 if d.get("hooksManager")=="global" else 1)' "$R/.git/dia/config.json" \
34
- && ok "config 记录 hooksManager=global" || bad "hooksManager 未记录"
26
+ hp="$(git -C "$R" config --local --get core.hooksPath)"
27
+ case "$hp" in */repo/.git/dia/hooks) ok "仓库本地 hooksPath 指向 dia 目录" ;; *) bad "hooksPath=$hp" ;; esac
28
+ for h in pre-commit pre-push post-checkout; do [ -x "$R/.git/dia/hooks/$h" ] || bad "缺 $h"; done
29
+ ok "三个钩子可执行"
30
+ python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));sys.exit(0 if d.get("hooksManager")=="local" else 1)' "$R/.git/dia/config.json" \
31
+ && ok "config 记录 hooksManager=local" || bad "hooksManager 未记录"
32
+ [ -z "$(git config --global --get core.hooksPath 2>/dev/null || true)" ] && ok "未写全局 git 配置" || bad "仍写了全局 hooksPath"
35
33
 
36
34
  bash "$WT" adopt "$R" --apply >/dev/null 2>&1 && ok "重复 apply 幂等" || bad "重复 apply 失败"
37
35
  bash "$WT" adopt "$R" --check >/dev/null 2>&1 && ok "check 通过" || bad "check 失败"
@@ -41,51 +39,55 @@ echo b > "$R/b.txt"; git -C "$R" add b.txt
41
39
  if git -C "$R" commit -qm should-block 2>/dev/null; then bad "主 checkout main 提交未被拦截"; else ok "主 checkout main 提交被拦截"; fi
42
40
  git -C "$R" reset -q
43
41
 
44
- # 链式:仓库自己的 .git/hooks/pre-commit 仍要执行(simple-git-hooks 等管理器就写这里)
45
- printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R/.git/hooks/pre-commit"
46
- chmod +x "$R/.git/hooks/pre-commit"
42
+ # 链式:仓库自己的 .git/hooks/pre-commit 仍要执行
43
+ printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R/.git/hooks/pre-commit"; chmod +x "$R/.git/hooks/pre-commit"
47
44
  git -C "$R" worktree add -q -b feat/y "$TD/repo-worktree--y" main
48
45
  echo c > "$TD/repo-worktree--y/c.txt"; git -C "$TD/repo-worktree--y" add c.txt
49
46
  git -C "$TD/repo-worktree--y" commit -qm feat-y >/dev/null 2>&1
50
47
  [ -f "$TD/repo-worktree--y/chain.log" ] && ok "仓库自己的钩子被链式执行" || bad "仓库钩子未执行"
51
48
  rm -f "$TD/repo-worktree--y/chain.log"
52
49
 
53
- # 本地 hooksPath(husky/.githooks)优先于全局 → 守卫不生效,但 adopt 不得破坏它
54
- R2="$TD/repo2"; mkdir -p "$R2/.githooks"; git -C "$R2" init -q -b main
55
- git -C "$R2" config user.email t@example.com; git -C "$R2" config user.name t
56
- echo a > "$R2/a.txt"; git -C "$R2" add -A; git -C "$R2" commit -qm init
57
- git -C "$R2" config --local core.hooksPath .githooks
58
- bash "$WT" adopt "$R2" --apply >/dev/null 2>&1
59
- [ "$(git -C "$R2" config --get core.hooksPath)" = ".githooks" ] && ok "保留仓库本地 hooksPath(husky 等)" || bad "破坏了本地 hooksPath"
60
- bash "$WT" doctor "$R2" 2>&1 | grep -q "本地 hooksPath" && ok "doctor 提示本地 hooksPath 覆盖全局" || bad "doctor 未提示覆盖"
50
+ # simple-git-hooks 仓:走 RC 注入,不碰仓内文件与 .git/hooks
51
+ RN="$TD/npmrepo"; mk_repo "$RN"
52
+ printf '{"name":"r","devDependencies":{"simple-git-hooks":"^2.11.1"},"simple-git-hooks":{"pre-commit":"npx lint-staged"}}\n' > "$RN/package.json"
53
+ git -C "$RN" add -A; git -C "$RN" commit -qm pkg
54
+ before="$(md5 -q "$RN/package.json" 2>/dev/null || md5sum "$RN/package.json" | cut -d' ' -f1)"
55
+ bash "$WT" adopt "$RN" --apply >/dev/null 2>&1 || bad "npm 仓 adopt 失败"
56
+ after="$(md5 -q "$RN/package.json" 2>/dev/null || md5sum "$RN/package.json" | cut -d' ' -f1)"
57
+ [ "$before" = "$after" ] && ok "不修改仓库 package.json" || bad "package.json 被改动"
58
+ [ -z "$(git -C "$RN" config --local --get core.hooksPath || true)" ] && ok "不占用仓库 hooksPath(交给管理器)" || bad "仍设置了 hooksPath"
59
+ [ -f "$DIA_ROOT/hooks/simple-git-hooks-rc.sh" ] && ok "写入 simple-git-hooks RC" || bad "缺 RC 文件"
60
+ grep -q "SIMPLE_GIT_HOOKS_RC" "$HOME/.zshenv" 2>/dev/null && ok "shell rc 注入 SIMPLE_GIT_HOOKS_RC" || bad "未注入环境变量"
61
+ bash "$WT" doctor "$RN" >/dev/null 2>&1 && ok "npm 仓 doctor 通过" || bad "npm 仓 doctor 失败"
62
+ (cd "$RN" && DIA_WT_GUARD_PROBE=1 bash "$WT" guard pre-commit >/dev/null 2>&1) && bad "guard 未拦截 main 提交" || ok "guard 拦截 main 提交"
61
63
 
62
- # 未 adopt 的仓:全局钩子纯透传(主 checkout 可正常提交)
63
- R3="$TD/repo3"; mkdir -p "$R3"; git -C "$R3" init -q -b main
64
- git -C "$R3" config user.email t@example.com; git -C "$R3" config user.name t
65
- echo a > "$R3/a.txt"; git -C "$R3" add -A
66
- git -C "$R3" commit -qm init >/dev/null 2>&1 && ok "未 adopt 仓不受守卫影响" || bad "未 adopt 仓被误拦"
64
+ # 全局配置残留检查:旧版全局钩子目录 → doctor FAIL
65
+ LEGACY="$TD/legacy-hooks"; mkdir -p "$LEGACY"
66
+ printf '#!/usr/bin/env bash\n# dia-wt guard(旧)\nexit 0\n' > "$LEGACY/pre-commit"; chmod +x "$LEGACY/pre-commit"
67
+ git config --global core.hooksPath "$LEGACY"
68
+ bash "$WT" doctor "$R" >/tmp/d20.log 2>&1; grep -q "D20" /tmp/d20.log && ok "doctor 报全局钩子残留" || bad "doctor 未报全局残留: $(head -3 /tmp/d20.log | tr "\n" " ")"
69
+ git config --global --unset core.hooksPath
67
70
 
68
- # 敏感扫描:adopted 仓的 worktree 推送带绝对路径 → 阻断;相对片段不误报
71
+ # 未 adopt 的仓不受影响
72
+ R3="$TD/repo3"; mk_repo "$R3"; echo z > "$R3/z.txt"; git -C "$R3" add -A
73
+ git -C "$R3" commit -qm ok >/dev/null 2>&1 && ok "未 adopt 仓不受守卫影响" || bad "未 adopt 仓被误拦"
74
+
75
+ # 敏感扫描(worktree push)
69
76
  BR="$TD/remote.git"; git init -q --bare "$BR"
70
77
  git -C "$R" remote add origin "$BR" 2>/dev/null || true
71
78
  git --git-dir="$BR" fetch -q "$R" main:main 2>/dev/null || true
72
79
  git -C "$R" fetch -q origin 2>/dev/null || true
73
80
  BAD="$(printf '/%s/alice/secret' Users)"; echo "$BAD" > "$TD/repo-worktree--y/bad.txt"
74
- git -C "$TD/repo-worktree--y" add bad.txt
75
- git -C "$TD/repo-worktree--y" commit -qm bad >/dev/null 2>&1
81
+ git -C "$TD/repo-worktree--y" add bad.txt; git -C "$TD/repo-worktree--y" commit -qm bad >/dev/null 2>&1
76
82
  if git -C "$TD/repo-worktree--y" push -q origin feat/y 2>/dev/null; then bad "敏感推送未被拦截"; else ok "敏感推送被拦截"; fi
77
- git -C "$TD/repo-worktree--y" reset -q --hard HEAD~1 # 丢掉含敏感内容的提交
83
+ git -C "$TD/repo-worktree--y" reset -q --hard HEAD~1
78
84
  printf 'path=$TD/home/kits/dia.json\n' > "$TD/repo-worktree--y/vars.txt"
79
85
  git -C "$TD/repo-worktree--y" add vars.txt && git -C "$TD/repo-worktree--y" commit -qm vars >/dev/null 2>&1
80
86
  git -C "$TD/repo-worktree--y" push -q origin feat/y 2>/dev/null && ok "相对片段 home/ 不误报" || bad "相对片段被误判"
81
87
 
82
- # adopt --all:只处理主 checkout,跳过 *-worktree--*
88
+ # adopt --all
83
89
  AR="$TD/allroot"; mkdir -p "$AR/a" "$AR/b"
84
- for r in a b; do
85
- git -C "$AR/$r" init -q -b main
86
- git -C "$AR/$r" config user.email t@example.com; git -C "$AR/$r" config user.name t
87
- echo x > "$AR/$r/x.txt"; git -C "$AR/$r" add -A; git -C "$AR/$r" commit -qm init
88
- done
90
+ for r in a b; do mk_repo "$AR/$r"; done
89
91
  git -C "$AR/a" worktree add -q -b feat/w "$AR/a-worktree--w" main
90
92
  out="$(DIA_WT_REPOS_ROOT="$AR" bash "$WT" adopt --all --apply --json 2>&1)"
91
93
  printf '%s' "$out" | grep -q '"repos":2,"ok":2,"fail":0' && ok "adopt --all 覆盖 2 个主 checkout" || bad "adopt --all: $out"
@@ -97,16 +99,16 @@ DIA_WT_REPOS_ROOT="$TD/empty-root" bash "$WT" adopt --all >/dev/null 2>&1
97
99
 
98
100
  # gate
99
101
  rm -f "$TD/repo-worktree--y/chain.log"
100
- if bash "$WT" gate "$R" y >/tmp/gate.log 2>&1; then ok "gate 成功"; else bad "gate 失败: $(tail -2 /tmp/gate.log | tr "\n" " ")"; fi
102
+ if bash "$WT" gate "$R" y >/tmp/gate.log 2>&1; then ok "gate 成功"; else bad "gate 失败: $(tail -2 /tmp/gate.log | tr '\n' ' ')"; fi
101
103
  if bash "$WT" gate "$R" yb >/dev/null 2>&1; then bad "gate 子串误匹配"; else ok "gate 精确匹配 feature"; fi
102
104
  echo dirty >> "$TD/repo-worktree--y/c.txt"
103
105
  if bash "$WT" gate "$R" y >/dev/null 2>&1; then bad "worktree 脏时 gate 通过"; else ok "worktree 脏时 gate 失败"; fi
104
106
  git -C "$TD/repo-worktree--y" checkout -q -- c.txt
105
107
 
106
- # revert:撤本仓配置,保留全局钩子
108
+ # revert
107
109
  bash "$WT" adopt "$R" --revert >/dev/null 2>&1 || bad "revert 失败"
108
110
  [ ! -f "$R/.git/dia/config.json" ] && ok "revert 删除本仓配置" || bad "配置残留"
109
- [ -x "$GH/pre-commit" ] && ok "revert 不影响全局钩子" || bad "revert 误删全局钩子"
111
+ [ -z "$(git -C "$R" config --local --get core.hooksPath || true)" ] && ok "revert 还原本地 hooksPath" || bad "hooksPath 未还原"
110
112
  git -C "$R" worktree remove --force "$TD/repo-worktree--y" >/dev/null 2>&1 || true
111
113
 
112
114
  echo; echo "── PASS $PASS · FAIL $FAIL ──"