@minonl/dia 0.0.5 → 0.0.7

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/cli.md CHANGED
@@ -100,7 +100,7 @@ dia persona remove dabo
100
100
  | `update [--check]` | **已实现** | 查 npm `dist-tags`;`--check` 有新版 exit 1、领先/最新 exit 0;**按安装方式分流**:npm 全局包 → `npm i -g`;脚本安装 → 提示 `git pull && bun run build && bash install.sh`(避免装到不在 PATH 的 npm prefix) |
101
101
  | `deps list / record` | **已实现** | `list` = 登记 + 实测派生视图(required 缺失 exit 2);`record` = 只追加一行(`--id/--kind/--version` 必填);`src/deps.ts` + `tests/deps*.test.ts` |
102
102
  | `doctor` | **已实现** | 组合自检:persona verify / **persona.render(宿主产物只读漂移,warn + 修复命令)** / default_agent / kit / knowledge(**索引新鲜度三态**,过期给重建命令)/ dsh preset / **deps 登记校验** / **platform 版本(读 `docs/platforms/registry.json`)** / wt;exit 0/1/2。不指定 persona 且装了多个 → **逐个自检**(条目加 `<id>.` 前缀),不再整体 `E_SCOPE` |
103
- | `wt …` | **已实现(需安装包)** | 转发 `packages/wt/bin/dia-wt`;未装时 `E_WT`(exit 33),`install.sh` 会分发 |
103
+ | `wt …` | **已实现(需安装包)** | 转发 `packages/wt/bin/dia-wt`(create/adopt/doctor/gate/list/status/check/clean/prune-merged;详见 [worktree.md](worktree.md));未装时 `E_WT`(exit 33),`install.sh` 会分发 |
104
104
  | `kb index/search/query/get/lint/path/review` | **已实现** | persona 作用域下索引过期自动重建;显式 `--root` 保持「缺索引即报错」 |
105
105
  | `kb <域命令>` | **已实现(委派)** | 未命中内置 → persona `commands` → 委派;argv 透传、exit code 原样 |
106
106
  | `<persona-cmd>` / `cmd <name>` | **已实现** | 一级委派(内置优先);`dia cmd` 只查外部命令,可绕开同名内置 |
package/docs/index.md CHANGED
@@ -23,6 +23,8 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
23
23
  | [dia monorepo:框架模块的归属](monorepo.md) | 结论:dia-job 回 dia monorepo,作为 packages/job;kb 作为 packages/kb。 |
24
24
  | [dia msg M0:送达与可读(设计 v0.1)](msg-collab-m0-design.md) | 一句话:任意两个活会话互发消息,收件方无需用户手敲命令即可读到;正文持久、有 ack/reply/close。 |
25
25
  | [dia msg M0 使用说明(送达与可读)](msg-collab-m0.md) | 一句话:任意两个活着的 dia 会话可以互发消息;收件方开工就能看到未读,正文不依赖临时文件。 |
26
+ | [dia msg M1:共享热点 claim + 任务依赖 + graph JSON(设计 v0.1)](msg-collab-m1-design.md) | 日期:2026-11-05 | 状态:设计(待实现) | 上游:docs/research/msg-collaboration-grill.md(C1–C3 / D1) |
27
+ | [dia claim:共享热点文件声明(M1a)](msg-collab-m1.md) | 一句话:在改共享文件前先 dia claim,让其它会话看见“谁在改、为什么、什么时候过期”;只告警,不阻断。 |
26
28
  | [dia / yan / dabo 现状总览](overview.md) | 2026-11-03 实测。本条是三方合起来的「现在长什么样」;docs/status.md 管 dia 的阶段与证据, |
27
29
  | [Patch:对已安装 persona / kit 的本地补丁](patching.md) | 参考 npm/patch-package,但只作用于本地安装副本,不改上游。 |
28
30
  | [Persona / Kit 安装:用 dia 从 git 地址装](persona-install.md) | 命名已定:persona = 可被选中的 agent 定义,kit = 知识/技能/工具内容,profile = 宿主运行环境(dsh)。 |
@@ -35,6 +37,7 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
35
37
  | [团队章程:谁来做 dia(开发团队)](team.md) | 2026-10-28 升级:开发团队执行模式改为全自动循环(计划→实现→验证→对抗→修复),人只在验收阶段介入; |
36
38
  | [版本与多会话(persona / kit)](versioning.md) | 问题:dia 要支持升级/本地迭代期间不同会话用不同版本 persona;kit 是否也要会话级版本? |
37
39
  | [dia 长期定位:从"安装器"到 superagent / agent-team 工厂](vision.md) | 2026-11-03,方向性文档。它不是现状——现状看 overview.md 与 status.md; |
40
+ | [worktree 纪律(`dia wt`)](worktree.md) | 目标:把「任何 git 改动走 worktree」从靠人记变成脚本可断言。 |
38
41
 
39
42
  ## 平台与宿主
40
43
 
@@ -0,0 +1,170 @@
1
+ # dia msg M1:共享热点 claim + 任务依赖 + graph JSON(设计 v0.1)
2
+
3
+ > 日期:2026-11-05 | 状态:**设计(待实现)** | 上游:`docs/research/msg-collaboration-grill.md`(C1–C3 / D1)
4
+ > 原则:** advisory,不阻断**;声明依赖与推断冲突分开;不做 daemon/TUI/全 worktree 锁。
5
+
6
+ ---
7
+
8
+ ## 0. 目标与非目标
9
+
10
+ | | 内容 |
11
+ |---|---|
12
+ | **目标** | ① 共享热点文件 `claim`(谁在改、谁让路);② `dia task` 支持 owner / deps / touches / closure_reason;③ `dia graph --json` 输出任务/claim 依赖图,环检测。 |
13
+ | **非目标** | 全 worktree 单写者锁、自动改代码/合并、TUI/Web、跨机同步、daemon。 |
14
+ | **成功判据** | 两个会话 claim 同一路径时第二个看到持有者;`dia task` 能回答“谁负责/依赖谁/碰哪些文件”;`dia graph` 能画出依赖并检出环。 |
15
+
16
+ **为什么先做 claim 而不是全 worktree 锁**:agent 改文件不经过 dia,锁没有执行点;claim 是“可见的承诺 + 开工告警”,比假锁诚实。
17
+
18
+ ### 0.1 开工闸门(planner 2026-11-05)
19
+
20
+ - M1 **以 M0 真实使用为前提**:两周内执行过 `dia in` 的会话 ≥30%,或至少有 3 个真实会话在用;未达则只定稿设计,不排实现。
21
+ - M1 拆两个 MR:**M1a claim**(本文件 §1)/ **M1b task+graph**(§2–§3);M1a 可先做,M1b 等 M1a 被真实使用。
22
+ - M1 不做:`dia in` 联动提示、`dia out --ref path=`、pre-commit/MR 强制点(这些留到 M2 或独立小 MR)。
23
+
24
+
25
+ ---
26
+
27
+ ## 1. 共享热点 claim
28
+
29
+ ### 1.1 数据
30
+
31
+ `~/.dia/collab/claims.jsonl`,append-only;一行一个事件:
32
+
33
+ ```json
34
+ {"ts":"...","event":"claim","id":"c-...","path":"install.sh","scope":"repo:/Users/min/repos/github/dia",
35
+ "holder":"ses_...","reason":"发布拆分","expires_at":"...","created_at":"..."}
36
+ {"ts":"...","event":"release","id":"c-...","holder":"ses_..."}
37
+ ```
38
+
39
+ - `path`:相对 `scope` 的规范化路径(去 `./`、`..` 归一化,不允许越出 scope);scope 用 **repo slug**(如 `github.com/minonl/dia`)而不是绝对路径,避免把用户名/机器路径写进 JSONL;不在 git 仓时用 cwd basename。
40
+ - claim 默认 **4h 过期**(agent 会话通常 <1h;`--ttl` 可改);过期视为不活跃,但保留历史。
41
+ - 同一 `(scope,path)` 允许多个活跃 claim(可见即警告);不排队、不阻断;`--force` 可接管已过期/失效 holder。
42
+ - `release` 必须由 holder 或过期后执行;其他会话不能 unclaim 别人的活跃 claim。
43
+ - `claims.jsonl` 用 O_EXCL 锁 append(复用 collab 的 `withLock` 方案);超过 5000 行时提示 compact。
44
+
45
+ ### 1.2 命令
46
+
47
+ ```bash
48
+ dia claim <path> [--reason R] [--ttl 4h] [--force] [--session S] [--json]
49
+ dia claims [--all] [--mine] [--json] # 默认只列活跃
50
+ dia unclaim <path|id> [--force] [--session S]
51
+ ```
52
+
53
+ - `dia claim` 成功时打印**现有 holder**(包括自己);有他人持有 → 输出 `⚠ overlap: ...`,退出码 **0**(advisory)。
54
+ - 只有 `E_NO_SESSION` / 路径非法 / 参数错才非 0。
55
+ - `dia claims` 人类输出:`path holder reason expires`;`--json` 返回数组;`--mine` 只列当前会话。
56
+
57
+ ### 1.3 不在 M1
58
+
59
+ - `dia in` 末尾追加 claim 提示、`dia out --ref path=`、pre-commit/MR 强制点:全部留到 M2;M1 只做 `claim/claims/unclaim` 命令。
60
+
61
+ ---
62
+
63
+ ## 2. `dia task` 扩展(不新建表)
64
+
65
+ 在现有 `tasks/ledger.jsonl` 上扩展事件与字段,**保持旧数据可读**:
66
+
67
+ ```json
68
+ {"event":"add","id":"t-...","title":"...","role":"builder","judge":"...",
69
+ "owner":"ses_...","deps":["t-..."],"touches":["src/collab.ts"],"status":"todo"}
70
+ {"event":"link","id":"t-...","deps":["t-..."],"touches":["..."]}
71
+ {"event":"status","id":"t-...","status":"blocked","evidence":"...","blocked_reason":"..."}
72
+ {"event":"status","id":"t-...","status":"done","evidence":"...","closure_reason":"done"}
73
+ ```
74
+
75
+ - `Task` 新增可选:`owner?: string`、`deps: string[]`、`touches: string[]`、`closure_reason?: string`、`blocked_reason?: string`。
76
+ - `done` 仍要求 judge+evidence;`closure_reason` 是**元数据**(默认 `done`),不新增终态。
77
+ - 终态收敛:`done` = 执行完成;`accepted` = team/human 验收(现有 team 流程);两者不互相替代,`closure_reason` 只解释 `done/dropped` 的原因。
78
+ - 依赖未完成时允许 `doing`,但 `dia graph` 显示 `blocked_by` 派生信息;不自动改状态。
79
+ - `dia task list` 人类输出增加 `owner` 与 `deps` 数量;`--json` 全字段;旧任务 `deps/touches` 缺省为 `[]`。
80
+
81
+ ### 2.1 命令
82
+
83
+ ```bash
84
+ dia task add "标题" [--owner S] [--depends t-a,t-b] [--touches path1,path2] [--judge J]
85
+ dia task link <id> [--depends t-a,t-b] [--touches path1,path2]
86
+ dia task status <id> done --evidence E [--reason done|dropped|duplicate|escalated|no-follow-on]
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 3. `dia graph`(先 JSON,后 UI)
92
+
93
+ ### 3.1 数据契约
94
+
95
+ ```json
96
+ {
97
+ "schema": 1,
98
+ "nodes": [
99
+ {"id":"task:t-1","kind":"task","label":"...","status":"doing","owner":"ses_..."},
100
+ {"id":"session:ses_...","kind":"session","label":"..."},
101
+ {"id":"path:src/collab.ts","kind":"path","label":"src/collab.ts"}
102
+ ],
103
+ "edges": [
104
+ {"from":"task:t-2","to":"task:t-1","kind":"depends_on","source":"declared"},
105
+ {"from":"session:ses_...","to":"path:src/collab.ts","kind":"claims","source":"declared"},
106
+ {"from":"task:t-1","to":"path:src/collab.ts","kind":"touches","source":"declared"}
107
+ ],
108
+ "cycles": [["task:t-1","task:t-2"]]
109
+ }
110
+ ```
111
+
112
+ - 边类型:`depends_on`(declared)、`touches`(declared)、`claims`(declared);M1 **不把 `blocked_by` 作为边**,只在节点/人类输出里派生显示“被谁挡着”。
113
+ - **不把推断冲突画成事实**:M1 只有 declared 边;后续 `inferred` 必须带 `source: inferred` + `confidence`。
114
+ - 环检测:任务依赖图有环 → `cycles` 非空,命令退出码 **3**(2 留给参数错;仍输出 JSON)。
115
+ - `--task <id>` 只输出该任务的出向 `depends_on` + `touches` + 直接 claim 邻居;不做全图闭包。
116
+
117
+ ### 3.2 命令
118
+
119
+ ```bash
120
+ dia graph [--json] [--task t-...] [--session S]
121
+ ```
122
+
123
+ - 默认输出全部 task + 活跃 claim + 相关 session/path;人类输出用缩进树(task → deps / touches)。
124
+ - `--task` 只输出该任务子图。
125
+
126
+ ---
127
+
128
+ ## 4. 验收
129
+
130
+ | # | 判据 | 证据 |
131
+ |---|---|---|
132
+ | V1 | 两个会话 claim 同一路径:第二个看到持有者与 `⚠ overlap`,exit 0;claims.jsonl 有两条 claim | CLI 输出 + JSONL 行数 |
133
+ | V2 | `--ttl 1s` 后 sleep,`dia claims` 活跃列表为空;`unclaim` 后立即消失 | 真实验证 |
134
+ | V3 | `dia task add --depends/--touches` 落 ledger;旧 task(无新字段)仍可读,deps/touches 为 `[]` | 单测 |
135
+ | V4 | `done` 仍强制 judge+evidence;closure_reason 可查,且不改变 accepted 语义 | 单测 |
136
+ | V5 | `dia graph --json` 有 task/session/path 节点与 declared 边;两次运行 diff 为空(稳定契约) | JSON fixture 断言 |
137
+ | V6 | 依赖环返回 `cycles` 非空 + **exit 3** | 单测 |
138
+ | V7 | 无 session 时明确 `E_NO_SESSION`,exit 非 0 | CLI 断言 |
139
+ | V8 | claim 并发 append 20 次不丢行、不坏行 | 并发单测 |
140
+ | V9 | `dia graph --json` 契约稳定(fixture `tests/fixtures/graph.v1.json`) | 单测 |
141
+
142
+ ---
143
+
144
+ ## 5. 实现拆分
145
+
146
+ ### M1a(claim,可先做)
147
+
148
+ 1. `src/claims.ts`:JSONL 读写 + `withLock` append、路径/scope 规范化、活跃过滤、TTL、`--force`。
149
+ 2. `packages/cli/src/index.ts`:`claim / claims / unclaim`;help 同步。
150
+ 3. `tests/claims.test.ts`:V1/V2/V7/V8。
151
+
152
+ ### M1b(task + graph,等 M1a 被真实使用)
153
+
154
+ 4. `src/task.ts`:fold 扩展 + `link` + `closure_reason`;保持旧事件兼容。
155
+ 5. `src/graph.ts`:从 task/claims 构建节点/边/环;`--task` 子图。
156
+ 6. `packages/cli/src/index.ts`:`task add/link` 新 flag;`graph`。
157
+ 7. 测试:`tests/graph.test.ts`、扩展 `tests/task.test.ts`;V3–V6/V9。
158
+ 8. 文档:`docs/msg-collab-m1.md` 使用说明;两批都要 `node scripts/docs-index.mjs`。
159
+
160
+ ---
161
+
162
+ ## 6. 风险
163
+
164
+ | 风险 | 缓解 |
165
+ |---|---|
166
+ | claim 被误解为锁 | 文档/输出明确 “advisory,不阻断”;不做自动拒绝 |
167
+ | 路径规范化不一致 | 统一 `resolve` + repo root 前缀;拒绝越出 scope |
168
+ | 旧 ledger 事件不兼容 | fold 对未知事件忽略;新字段 optional |
169
+ | graph 变成“事实” | declared/inferred 分色;M1 只有 declared |
170
+ | 使用率低 | `dia claims` 先做“可见”,不做强制门禁 |
@@ -0,0 +1,66 @@
1
+ # dia claim:共享热点文件声明(M1a)
2
+
3
+ > 一句话:在改共享文件前先 `dia claim`,让其它会话看见“谁在改、为什么、什么时候过期”;只告警,不阻断。
4
+ > 设计:`docs/msg-collab-m1-design.md` §1
5
+
6
+ ## 什么时候用
7
+
8
+ - 要改 `install.sh`、`AGENTS.md`、`launchd/`、`docs/index.md` 等**多个会话都可能碰**的文件;
9
+ - 交接/并行开发前,先声明路径,避免“改完才发现别人也在改”。
10
+
11
+ ## 命令
12
+
13
+ ```bash
14
+ dia claim install.sh --reason "发布拆分" --ttl 4h
15
+ dia claims # 当前活跃 claim
16
+ dia claims --mine # 只看自己的
17
+ dia claims --all # 含已过期
18
+ dia unclaim install.sh # 释放自己的
19
+ ```
20
+
21
+ - claim 只在 `~/.dia/collab/claims.jsonl` 追加事实,**不锁文件、不排队、不阻断编辑**;
22
+ - 同一路径多人 claim 时,第二个会看到 `⚠ overlap: <holder>`,退出码仍是 0;
23
+ - 默认 4h 过期;`--ttl 30m` / `1d` 可调;
24
+ - 只能释放自己的 claim;`--force` 用于清理别人的过期 claim(明确的人工操作)。
25
+
26
+ ## 路径与 scope
27
+
28
+ - scope 用仓库标识(如 `github.com/minonl/dia`),不写绝对路径;
29
+ - `path` 相对仓库根;`../` 越界会报 `E_CLAIM_PATH`;
30
+ - 不在 git 仓时用当前目录 basename,scope 为 `local:<dir>`。
31
+
32
+ ## 边界
33
+
34
+ - 这是 **advisory**:不保证别人真的让路,也不自动改代码/合并;
35
+ - 不做全 worktree 锁;文件系统写操作仍以人工/MR 协调为准;
36
+ - M1a 不含 `dia in` 提示、pre-commit/MR 强制点(留到后续)。
37
+
38
+ ---
39
+
40
+ # dia task 依赖 / dia graph(M1b)
41
+
42
+ ## 任务声明
43
+
44
+ ```bash
45
+ dia task add "实现 X" --owner ses_... --depends t-a,t-b --touches src/x.ts,docs/x.md --judge "bun test"
46
+ dia task link t-xxx --depends t-new --touches src/y.ts
47
+ dia task link t-xxx --remove-depends t-old
48
+ dia task done t-xxx --evidence "bun test 通过" --reason done
49
+ ```
50
+
51
+ - `owner` / `deps` / `touches` 都是可选;旧 task 没有这些字段时自动为 `[]`。
52
+ - `done` 仍强制 judge + evidence;`--reason` 只记录 closure_reason,不新增终态。
53
+ - `pending`/`blocked` 用 `task status <id> blocked --blocked-reason ...`。
54
+
55
+ ## 依赖图
56
+
57
+ ```bash
58
+ dia graph --json
59
+ dia graph # 人类可读:task → deps + claims
60
+ dia graph --json --task t-xxx
61
+ ```
62
+
63
+ - 只输出 **declared** 边:`depends_on` / `touches` / `claims`;
64
+ - `blocked_by` 只在 task 节点里做派生提示,不是边;
65
+ - 任务依赖成环:`cycles` 非空,退出码 **3**(参数错仍是 2);
66
+ - `--task` 只输出该任务的直接依赖/touches/claim 邻居,不做全图闭包。
@@ -0,0 +1,106 @@
1
+ # worktree 纪律(`dia wt`)
2
+
3
+ > 目标:把「任何 git 改动走 worktree」从**靠人记**变成**脚本可断言**。
4
+ > 实现:`packages/wt/bin/dia-wt`(`install.sh` 分发到 `<install>/packages/wt`,`dia wt` 转发)。
5
+
6
+ ## 1. 一条命令接管仓库
7
+
8
+ ```bash
9
+ dia wt adopt <repo> # 默认 dry-run:打印计划,不落盘
10
+ dia wt adopt <repo> --apply # 写入配置 + 三个守卫钩子
11
+ dia wt adopt --all --apply # 批量:仓根下所有主 checkout(跳过 *-worktree--*)
12
+ ```
13
+
14
+ 写入内容(幂等):
15
+
16
+ | 项 | 位置 |
17
+ |---|---|
18
+ | 策略 | `<git-common-dir>/dia/config.json`(`defaultBranch` / `template` / `hooksPath` / `hooksPrevious` / `hooksChain`) |
19
+ | 钩子 | `<git-common-dir>/dia/hooks/{pre-commit,pre-push,post-checkout}` |
20
+ | `core.hooksPath` | 指向上面目录,**必须是绝对路径**(相对路径在 worktree 会静默失效) |
21
+
22
+ 守卫行为:
23
+
24
+ | 钩子 | 规则 |
25
+ |---|---|
26
+ | `pre-commit` | 主 checkout 上、分支属于 protected(默认分支 + main/master/develop/dev)→ 拒绝 |
27
+ | `pre-push` | 主 checkout 上**任何 push**(含 tag / feature 分支)→ 拒绝;推 `main/master/develop/dev` → 拒绝 |
28
+ | `post-checkout` | 主 checkout 切到非默认分支 → 黄色提示(不阻断) |
29
+
30
+ **钩子链**:`hooksChain`(首次 adopt 前的 hooksPath,如 `.husky` / `.githooks`)→ `<common>/hooks` → 默认放行。
31
+ 所以 husky / simple-git-hooks / lefthook 的钩子不会被吞。
32
+
33
+ > ⚠️ `simple-git-hooks` 的 `postinstall` 会重写 `core.hooksPath` 下的钩子。install 后若守卫被覆盖:
34
+ > `dia wt doctor --fix`(或重跑 `dia wt adopt --apply`)。
35
+
36
+ ## 2. 巡检
37
+
38
+ ```bash
39
+ dia wt doctor <repo>... # 单仓/多仓
40
+ dia wt doctor --all # 扫描仓根(<data>/repos、$DIA_WT_REPOS_ROOT,默认 ~/repos)
41
+ dia wt doctor --all --fix # 无损修复:重装三个钩子 + 校正 hooksPath(不删分支/不动 worktree/不提交)
42
+ ```
43
+
44
+ 判据(FAIL 会让进程 exit 1):
45
+
46
+ | ID | 断言 |
47
+ |---|---|
48
+ | D01 | 已 adopt(`config.json` 存在) |
49
+ | D02 | `core.hooksPath` 绝对路径,三个钩子可执行 |
50
+ | D03 | 钩子静态包含默认分支(`prot=`),动态探针能拦住主 checkout 的默认分支提交 |
51
+ | D04 | 主 checkout 干净且位于默认分支(WARN) |
52
+ | D05 | worktree 路径都存在 |
53
+ | D06 | 无 prunable / locked |
54
+ | D07 | 同一分支不被多个 worktree 占用 |
55
+ | D11 | 默认分支可解析(config > origin/HEAD(校验仍存在)> main > master) |
56
+
57
+ 挂成日巡检(mac launchd / linux cron 由 `dia job` 统一):
58
+
59
+ ```bash
60
+ dia job install dia-wt-doctor --calendar 09:00 \
61
+ --cmd "$HOME/.local/bin/dia wt doctor --all --json" \
62
+ --desc "worktree 纪律巡检" --allow-unstable
63
+ ```
64
+
65
+ ## 3. 提交 MR 前:`gate`
66
+
67
+ ```bash
68
+ dia wt gate <repo> <feature>
69
+ ```
70
+
71
+ 顺序:`check`(依赖体检 R20)→ `doctor` → 目标 worktree 无未提交改动 → `ahead origin/<default> > 0`。
72
+ **只做本地门禁,不做 merge / push**——合并仍走 MR + `@bot mergeme`。
73
+
74
+ ## 4. 日常命令
75
+
76
+ ```bash
77
+ dia wt create <repo> <feature> [--base main] [--install] [--dry-run]
78
+ dia wt list [<repo>]
79
+ dia wt status [<repo>] [--stale-days N]
80
+ dia wt check [<repo>...] [--all] [--deep]
81
+ dia wt clean <repo> <feature|--branch B> [--force] [--keep-branch]
82
+ dia wt prune-merged [<repo>] [--all] [--apply]
83
+ ```
84
+
85
+ 路径模板默认 `{{parent}}/{{repo}}-worktree--{{feature}}`(与历史布局一致,零迁移);
86
+ `adopt --template` 可改(支持 `{{parent}}` `{{repo}}` `{{feature}}` `{{wtRoot}}`),`create/list/clean` 都按它解析。
87
+
88
+ ## 5. 退出码
89
+
90
+ | 码 | 含义 |
91
+ |---|---|
92
+ | 0 | 干净 / 成功 |
93
+ | 1 | 违规或漂移(doctor FAIL、gate 未通过、check 有违规) |
94
+ | 2 | 用法错误(含 `--all` 未匹配到仓库) |
95
+ | 3 | 未 adopt |
96
+ | 4 | worktree 歧义(多个候选) |
97
+
98
+ ## 6. 没有逃生口:钩子慢就优化
99
+
100
+ 本仓**不提供**跳过钩子的开关,也不在文档/脚本里给绕过示例:
101
+
102
+ - 钩子执行慢 → **先量**(`time git commit` / `time git push`),定位到具体钩子;
103
+ - 优化手段:缓存(如 lint 增量、`nx affected`)、把重活下沉到 CI、减少扫描范围;
104
+ - 优化前宁可短暂变慢,也不跳过——一次跳过等于把质量门禁变成装饰。
105
+
106
+ 自动化脚本(release 等)同理:**必须在 worktree 里跑**,不得在主 checkout 上做提交/推送。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minonl/dia",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "description": "dia (嗲) — lightweight agent framework: knowledge index, audit, session ingest",
6
6
  "bin": {
@@ -215,6 +215,31 @@ JS
215
215
  }
216
216
 
217
217
  # adopt 写入的路径模板(<git-common-dir>/dia/config.json 的 template);未 adopt → 空
218
+ # 仓根集合:<data>/repos + DIA_WT_REPOS_ROOTS(冒号分隔;默认 ~/repos + ~/.yan-agent/repos)
219
+ wt_repo_roots() {
220
+ local dr r
221
+ local -a out=()
222
+ dr="$(dia_wt_data_root 2>/dev/null || true)"
223
+ [ -n "$dr" ] && [ -d "$dr/repos" ] && out+=("$dr/repos")
224
+ local rr="${DIA_WT_REPOS_ROOTS:-${DIA_WT_REPOS_ROOT:-$HOME/repos:$HOME/.yan-agent/repos}}"
225
+ while IFS= read -r r; do [ -n "$r" ] && [ -d "$r" ] && out+=("$r"); done < <(printf '%s\n' "$rr" | tr ':' '\n')
226
+ [ ${#out[@]} -gt 0 ] && printf '%s\n' "${out[@]}"
227
+ return 0
228
+ }
229
+ # 所有主 checkout(git-dir == common-dir);跳过 *-worktree--*
230
+ wt_discover_main_checkouts() {
231
+ local root sub gd cd_
232
+ while IFS= read -r root; do
233
+ while IFS= read -r -d '' gd; do
234
+ sub="$(dirname "$gd")"
235
+ case "$(basename "$sub")" in *-worktree--*) continue ;; esac
236
+ cd_="$(git -C "$sub" rev-parse --git-common-dir 2>/dev/null || true)"
237
+ gd="$(cd "$sub" 2>/dev/null && cd "$gd" 2>/dev/null && pwd -P)"
238
+ cd_="$(cd "$sub" 2>/dev/null && cd "$cd_" 2>/dev/null && pwd -P)"
239
+ [ -n "$gd" ] && [ "$gd" = "$cd_" ] && printf '%s\n' "$sub"
240
+ done < <(find "$root" -maxdepth 3 -name .git -type d -print0 2>/dev/null)
241
+ done < <(wt_repo_roots)
242
+ }
218
243
  wt_repo_template() {
219
244
  local repo="$1" common cfg
220
245
  common="$(wt_common_dir "$repo" 2>/dev/null)" || return 1
@@ -1259,26 +1284,8 @@ cmd_adopt() {
1259
1284
  esac; done
1260
1285
  if [ "$all" = 1 ]; then
1261
1286
  [ -z "$repo" ] || die "--all 不能与 <repo> 同时给(要么单个仓,要么全量)"
1262
- local -a roots=() repos=()
1263
- local dr rr
1264
- dr="$(dia_wt_data_root 2>/dev/null || true)"
1265
- rr="${DIA_WT_REPOS_ROOT:-$HOME/repos}"
1266
- [ -n "$dr" ] && [ -d "$dr/repos" ] && roots+=("$dr/repos")
1267
- [ -d "$rr" ] && roots+=("$rr")
1268
- [ ${#roots[@]} -gt 0 ] || { echo "usage: dia wt adopt --all(未找到仓根:${rr})" >&2; exit 2; }
1269
- local root sub gd cd_
1270
- for root in "${roots[@]}"; do
1271
- while IFS= read -r -d '' gd; do
1272
- sub="$(dirname "$gd")"
1273
- case "$(basename "$sub")" in *-worktree--*) continue ;; esac
1274
- cd_="$(git -C "$sub" rev-parse --git-common-dir 2>/dev/null || true)"
1275
- # 统一物理路径(macOS /tmp → /private/tmp),否则等式比较会假失败
1276
- gd="$(cd "$sub" 2>/dev/null && cd "$gd" 2>/dev/null && pwd -P)"
1277
- cd_="$(cd "$sub" 2>/dev/null && cd "$cd_" 2>/dev/null && pwd -P)"
1278
- [ -n "$gd" ] && [ "$gd" = "$cd_" ] || continue # 只处理主 checkout
1279
- repos+=("$sub")
1280
- done < <(find "$root" -maxdepth 3 -name .git -type d -print0 2>/dev/null)
1281
- done
1287
+ local -a repos=()
1288
+ while IFS= read -r r; do repos+=("$r"); done < <(wt_discover_main_checkouts)
1282
1289
  [ ${#repos[@]} -gt 0 ] || { echo "usage: dia wt adopt --all(未匹配到任何主 checkout)" >&2; exit 2; }
1283
1290
  local -a pass=()
1284
1291
  local a
@@ -1400,7 +1407,7 @@ cmd_doctor() {
1400
1407
  *) repos+=("$1"); shift ;;
1401
1408
  esac; done
1402
1409
  if [ "$all" = 1 ] && [ ${#repos[@]} -eq 0 ]; then
1403
- while IFS= read -r -d '' d; do repos+=("$(dirname "$d")"); done < <(find "${HOME}/repos" -maxdepth 3 -name .git -type d -print0 2>/dev/null)
1410
+ while IFS= read -r d; do repos+=("$d"); done < <(wt_discover_main_checkouts)
1404
1411
  fi
1405
1412
  if [ ${#repos[@]} -eq 0 ]; then
1406
1413
  echo "usage: dia wt doctor <repo>... | --all(--all 未匹配到任何仓库)" >&2
@@ -10,7 +10,7 @@ ok(){ echo " ✓ $1"; PASS=$((PASS+1)); }; bad(){ echo " ✗ $1"; FAIL=$((FAIL
10
10
 
11
11
  R="$TD/repo"; mkdir -p "$R"; git -C "$R" init -q -b main
12
12
  git -C "$R" config user.email t@example.com; git -C "$R" config user.name t
13
- echo a > "$R/a.txt"; git -C "$R" add -A; git -C "$R" commit -qm init --no-verify
13
+ echo a > "$R/a.txt"; git -C "$R" add -A; git -C "$R" commit -qm init
14
14
 
15
15
  out="$(bash "$WT" adopt "$R" --json)"
16
16
  printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "dry-run 识别默认分支" || bad "dry-run: $out"
@@ -59,7 +59,7 @@ bash "$WT" adopt "$R" --revert >/dev/null 2>&1 || bad "revert 失败"
59
59
  # ── 原 hooksPath 保留 + 二次 apply 不改写 hooksPrevious(review P2-7)──────────
60
60
  R2="$TD/repo2"; mkdir -p "$R2" "$TD/orighooks"; git -C "$R2" init -q -b main
61
61
  git -C "$R2" config user.email t@example.com; git -C "$R2" config user.name t
62
- echo a > "$R2/a.txt"; git -C "$R2" add -A; git -C "$R2" commit -qm init --no-verify
62
+ echo a > "$R2/a.txt"; git -C "$R2" add -A; git -C "$R2" commit -qm init
63
63
  printf '#!/bin/sh\nexit 0\n' > "$TD/orighooks/pre-commit"; chmod +x "$TD/orighooks/pre-commit"
64
64
  git -C "$R2" config --local core.hooksPath "$TD/orighooks"
65
65
  bash "$WT" adopt "$R2" --apply >/dev/null 2>&1 || bad "R2 apply 失败"
@@ -80,7 +80,7 @@ git -C "$R2" config --local core.hooksPath "$R2/.git/dia/hooks"; bash "$WT" adop
80
80
  # ── 自定义默认分支受保护(review P1-1 + P1-2)────────────────────────────────
81
81
  R3="$TD/repo3"; mkdir -p "$R3"; git -C "$R3" init -q -b trunk
82
82
  git -C "$R3" config user.email t@example.com; git -C "$R3" config user.name t
83
- echo a > "$R3/a.txt"; git -C "$R3" add -A; git -C "$R3" commit -qm init --no-verify
83
+ echo a > "$R3/a.txt"; git -C "$R3" add -A; git -C "$R3" commit -qm init
84
84
  bash "$WT" adopt "$R3" --apply --default-branch trunk >/dev/null 2>&1 || bad "自定义分支 apply 失败"
85
85
  bash "$WT" adopt "$R3" --check >/dev/null 2>&1 && ok "自定义默认分支 check 通过" || bad "自定义默认分支 check 失败"
86
86
  bash "$WT" doctor "$R3" >/dev/null 2>&1 && ok "自定义默认分支 doctor 通过" || bad "自定义默认分支 doctor 失败"
@@ -92,7 +92,7 @@ git -C "$R3" reset -q
92
92
  bash "$WT" doctor "$R3" >/dev/null 2>&1 || true
93
93
  R4="$TD/repo4"; mkdir -p "$R4"; git -C "$R4" init -q -b main
94
94
  git -C "$R4" config user.email t@example.com; git -C "$R4" config user.name t
95
- echo a > "$R4/a.txt"; git -C "$R4" add -A; git -C "$R4" commit -qm init --no-verify
95
+ echo a > "$R4/a.txt"; git -C "$R4" add -A; git -C "$R4" commit -qm init
96
96
  if bash "$WT" doctor "$R4" >/dev/null 2>&1; then bad "未 adopt 的仓 doctor 误报通过"; else ok "未 adopt 的仓 doctor 失败"; fi
97
97
  bash "$WT" doctor >/dev/null 2>&1; [ $? = 2 ] && ok "doctor 无参数 exit 2" || bad "doctor 无参数未 exit 2"
98
98
 
@@ -100,17 +100,18 @@ bash "$WT" doctor >/dev/null 2>&1; [ $? = 2 ] && ok "doctor 无参数 exit 2" ||
100
100
  R5="$TD/repo5"; ORIGIN5="$TD/origin5.git"; git init -q --bare "$ORIGIN5"
101
101
  mkdir -p "$R5"; git -C "$R5" init -q -b main
102
102
  git -C "$R5" config user.email t@example.com; git -C "$R5" config user.name t
103
- echo a > "$R5/a.txt"; git -C "$R5" add -A; git -C "$R5" commit -qm init --no-verify
104
- git -C "$R5" remote add origin "$ORIGIN5"; git -C "$R5" push -q --no-verify origin main
103
+ echo a > "$R5/a.txt"; git -C "$R5" add -A; git -C "$R5" commit -qm init
104
+ git -C "$R5" remote add origin "$ORIGIN5"; git -C "$R5" push -q origin main
105
105
  git -C "$R5" fetch -q origin
106
106
  git -C "$R5" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
107
107
  bash "$WT" adopt "$R5" --apply >/dev/null 2>&1 || bad "R5 apply 失败"
108
- echo x >> "$R5/a.txt"; git -C "$R5" commit -qam dirty-main --no-verify 2>/dev/null
109
- if git -C "$R5" push origin main >/dev/null 2>&1; then bad "主 checkout push 未被拦截"; else ok "主 checkout push 被拦截"; fi
110
- git -C "$R5" reset -q --hard origin/main
108
+ # 主 checkout 的 pre-push 拒绝的是「从这里发起的任何 push」(含 feature 分支),不需要脏提交
109
+ git -C "$R5" branch feat/push-probe origin/main
110
+ if git -C "$R5" push origin feat/push-probe >/dev/null 2>&1; then bad "主 checkout push 未被拦截"; else ok "主 checkout push 被拦截"; fi
111
+ git -C "$R5" branch -D feat/push-probe >/dev/null 2>&1 || true
111
112
  git -C "$R5" worktree add -q -b feat/x "$TD/repo5-worktree--x" main
112
113
  echo b > "$TD/repo5-worktree--x/b.txt"; git -C "$TD/repo5-worktree--x" add b.txt
113
- git -C "$TD/repo5-worktree--x" commit -qm feat-x --no-verify 2>/dev/null
114
+ git -C "$TD/repo5-worktree--x" commit -qm feat-x 2>/dev/null
114
115
  if git -C "$TD/repo5-worktree--x" push -q origin feat/x >/dev/null 2>&1; then ok "worktree push 放行"; else bad "worktree push 被误拦"; fi
115
116
 
116
117
  # ── template 生效(P2-6):adopt 后 create 按 config.template 推导路径 ─────────
@@ -124,7 +125,7 @@ bash "$WT" adopt "$R5" --apply >/dev/null 2>&1
124
125
  # ── 过期 origin/HEAD(远端分支已删)不误判默认分支 ──────────────────────────
125
126
  R6="$TD/repo6"; mkdir -p "$R6"; git -C "$R6" init -q -b main
126
127
  git -C "$R6" config user.email t@example.com; git -C "$R6" config user.name t
127
- echo a > "$R6/a.txt"; git -C "$R6" add -A; git -C "$R6" commit -qm init --no-verify
128
+ echo a > "$R6/a.txt"; git -C "$R6" add -A; git -C "$R6" commit -qm init
128
129
  git -C "$R6" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/gone
129
130
  out="$(bash "$WT" adopt "$R6" --json)"
130
131
  printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "过期 origin/HEAD 回落到 main" || bad "过期 origin/HEAD: $out"
@@ -134,7 +135,7 @@ R7="$TD/repo7"; mkdir -p "$R7/.githooks"; git -C "$R7" init -q -b main
134
135
  git -C "$R7" config user.email t@example.com; git -C "$R7" config user.name t
135
136
  printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R7/.githooks/pre-commit"
136
137
  chmod +x "$R7/.githooks/pre-commit"
137
- echo a > "$R7/a.txt"; git -C "$R7" add -A; git -C "$R7" commit -qm init --no-verify
138
+ echo a > "$R7/a.txt"; git -C "$R7" add -A; git -C "$R7" commit -qm init
138
139
  git -C "$R7" config --local core.hooksPath .githooks
139
140
  bash "$WT" adopt "$R7" --apply >/dev/null 2>&1 || bad "R7 apply 失败"
140
141
  python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));sys.exit(0 if d.get("hooksChain")==".githooks" else 1)' "$R7/.git/dia/config.json" \
@@ -151,7 +152,7 @@ AR="$TD/allroot"; mkdir -p "$AR/a" "$AR/b"
151
152
  for r in a b; do
152
153
  git -C "$AR/$r" init -q -b main
153
154
  git -C "$AR/$r" config user.email t@example.com; git -C "$AR/$r" config user.name t
154
- echo x > "$AR/$r/x.txt"; git -C "$AR/$r" add -A; git -C "$AR/$r" commit -qm init --no-verify
155
+ echo x > "$AR/$r/x.txt"; git -C "$AR/$r" add -A; git -C "$AR/$r" commit -qm init
155
156
  done
156
157
  git -C "$AR/a" worktree add -q -b feat/w "$AR/a-worktree--w" main
157
158
  out="$(DIA_WT_REPOS_ROOT="$AR" DIA_WT_DATA_ROOT="$TD/dr2" bash "$WT" adopt --all --apply --json 2>&1)"
@@ -160,13 +161,15 @@ for r in a b; do
160
161
  [ -n "$(git -C "$AR/$r" config --get core.hooksPath || true)" ] && ok "adopt --all: $r 已配 hooksPath" || bad "adopt --all: $r 未配置"
161
162
  done
162
163
  [ ! -d "$AR/a-worktree--w/.git/dia" ] && ok "adopt --all 跳过 worktree 目录" || bad "adopt --all 误改 worktree"
164
+ out="$(DIA_WT_REPOS_ROOT="$AR" DIA_WT_DATA_ROOT="$TD/dr4" bash "$WT" doctor --all --json 2>/dev/null)"
165
+ printf '%s' "$out" | grep -q "$AR/a" && ok "doctor --all 覆盖 DIA_WT_REPOS_ROOT" || bad "doctor --all 未覆盖自定义仓根"
163
166
  DIA_WT_REPOS_ROOT="$TD/empty-root" DIA_WT_DATA_ROOT="$TD/dr3" bash "$WT" adopt --all >/dev/null 2>&1
164
167
  [ $? = 2 ] && ok "adopt --all 空结果 exit 2" || bad "adopt --all 空结果未 exit 2"
165
168
 
166
169
  # ── doctor --fix:钩子被覆盖后能无损重建 ────────────────────────────────────
167
170
  R8="$TD/repo8"; mkdir -p "$R8"; git -C "$R8" init -q -b main
168
171
  git -C "$R8" config user.email t@example.com; git -C "$R8" config user.name t
169
- echo a > "$R8/a.txt"; git -C "$R8" add -A; git -C "$R8" commit -qm init --no-verify
172
+ echo a > "$R8/a.txt"; git -C "$R8" add -A; git -C "$R8" commit -qm init
170
173
  bash "$WT" adopt "$R8" --apply >/dev/null 2>&1
171
174
  rm -f "$R8/.git/dia/hooks/post-checkout"
172
175
  printf '#!/bin/sh\nexit 0\n' > "$R8/.git/dia/hooks/pre-commit"; chmod +x "$R8/.git/dia/hooks/pre-commit"