task-ledger 1.3.3 → 1.3.5
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 +2 -2
- package/SKILL.md +32 -19
- package/package.json +1 -1
- package/references/state-model.md +9 -7
- package/references/troubleshooting.md +19 -7
- package/scripts/ledgerkit/actions/atom.py +2 -2
- package/scripts/ledgerkit/actions/bootstrap.py +9 -2
- package/scripts/ledgerkit/actions/claim.py +7 -6
- package/scripts/ledgerkit/actions/close_task.py +13 -12
- package/scripts/ledgerkit/actions/guard.py +38 -16
- package/scripts/ledgerkit/actions/handoff.py +19 -8
- package/scripts/ledgerkit/actions/init_identity.py +90 -0
- package/scripts/ledgerkit/actions/list_tasks.py +21 -3
- package/scripts/ledgerkit/actions/log_progress.py +5 -2
- package/scripts/ledgerkit/actions/new_task.py +11 -7
- package/scripts/ledgerkit/actions/query.py +4 -2
- package/scripts/ledgerkit/actions/query_history.py +48 -9
- package/scripts/ledgerkit/actions/registry.py +1 -1
- package/scripts/ledgerkit/actions/release.py +25 -4
- package/scripts/ledgerkit/actions/set_task.py +18 -7
- package/scripts/ledgerkit/actions/validate.py +0 -4
- package/scripts/ledgerkit/core/history_md.py +20 -245
- package/scripts/ledgerkit/core/identity.py +20 -7
- package/scripts/ledgerkit/core/labels.py +0 -1
- package/scripts/ledgerkit/core/paths.py +0 -1
- package/scripts/ledgerkit/env.py +70 -71
- package/scripts/ledgerkit/shell/cli.py +60 -3
- package/scripts/ledgerkit/shell/gitops.py +14 -1
- package/scripts/ledgerkit/shell/render.py +3 -2
- package/scripts/ledgerkit/shell/runner.py +1 -1
- package/scripts/ledgerkit/store/history_store.py +23 -80
- package/scripts/ledgerkit/store/owners_index.py +16 -3
- package/scripts/ledgerkit/store/task_store.py +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| 文件 | 装什么 |
|
|
18
18
|
|---|---|
|
|
19
19
|
| `ledger/pool.json` | 公共池:未认领任务(owner 空) |
|
|
20
|
-
| `ledger/owners.json` |
|
|
20
|
+
| `ledger/owners.json` | 索引:身份 uuid → 分片文件(`name` 只做显示 / 模糊筛) |
|
|
21
21
|
| `ledger/owners/<hash>.json` | 每人一份分片:自己认领的任务 |
|
|
22
22
|
| `ledger/worklog/<年月>/*.md` | 任务正文(六节) |
|
|
23
23
|
| `ledger/history/<年月>.md` | 已结案(一行一条,9 列) |
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
python3 <SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工作区根>
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
子命令:`new` `claim` `set` `log` `close` `list` `history` `show` `
|
|
37
|
+
子命令:`new` `claim` `set` `log` `close` `list` `history` `show` `validate`。命令参考(参数 / 一句话 / 举例)、任务生命周期、纪律(数据完整性 / 人类授权 / 故障处置)全在 `SKILL.md`;参数细节看 `<子命令> --help`(不要凭记忆编)。
|
|
38
38
|
|
|
39
39
|
## 深入参考(references/,按需读)
|
|
40
40
|
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: task-ledger
|
|
3
3
|
description: 本地任务台账,跨会话记录在办任务的归属、进度与起止。当用户要登记一件新的事(新建任务 / 建个坑 / 这活我领了)、记录已有任务的进展(接着做 / 记一条进展 / 这轮先记下)、查看任务状态或历史(现在该做什么 / 这活谁在做 / 上周做过什么)、给任务结案或退回(做完了 / 验收通过 / 不做了 / 退回待认领)、整理标签或检查台账一致性时加载;只是改代码、讨论方案时不要用。
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.5
|
|
5
5
|
allowed-tools: Read,Edit,Bash,Grep
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -21,7 +21,17 @@ allowed-tools: Read,Edit,Bash,Grep
|
|
|
21
21
|
|
|
22
22
|
## 命令参考
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
十个子命令:`init`(初始化本机身份,一台机器一次)+ 前五个写命令(`new`/`claim`/`set`/`log`/`close`)+ 后四个查询类(`list` / `show` / `history` / `validate`);何时用哪个见「任务生命周期」。参数语法看 `<子命令> --help`;下表说每个参数干什么、何时用。
|
|
25
|
+
|
|
26
|
+
### init — 初始化本机身份
|
|
27
|
+
|
|
28
|
+
| 参数 | 含义 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| (不带参数) | 只有当前仓库 `owners.json` 里已有人时才列出候选 uuid 供挑选;名册为空时直接报错 |
|
|
31
|
+
| `--claim <uuid>` | 认领名册里已有的 uuid(名字一起复制) |
|
|
32
|
+
| `--new --name "…"` | 新建身份:生成 uuid + 名字 |
|
|
33
|
+
|
|
34
|
+
干什么:写本机身份文件(`~/.taskledger/identity.json`),**一台机器只一次**,与台账无关。全新机器或新项目没有名册 ⇒ 直接用 `init --new --name "<名字>"`,裸跑 `init` 会报「新建身份必须输入一个名字」。
|
|
25
35
|
|
|
26
36
|
### new — 建任务
|
|
27
37
|
|
|
@@ -54,12 +64,12 @@ allowed-tools: Read,Edit,Bash,Grep
|
|
|
54
64
|
| 参数 | 含义 |
|
|
55
65
|
|---|---|
|
|
56
66
|
| `--id <任务号>` | 释放哪个(必填) |
|
|
57
|
-
| `--
|
|
58
|
-
| `--owner
|
|
67
|
+
| `--release` | 释放回公共池(不带人) |
|
|
68
|
+
| `--owner <别人的uuid>` | 把任务转交给对方(预留通道,**agent 不用**,见纪律;只收 uuid,传名字当场报错) |
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
干什么:把任务放回公共池、状态回「待认领」,别人即可认领;没有单独的「改状态」入口。**`--release` 与 `--owner` 必须二选一、不能同给**(只给 `--id` 会报错)。
|
|
61
71
|
|
|
62
|
-
举例:用户明确说「把这个任务退回去」→ `set --id <任务号> --
|
|
72
|
+
举例:用户明确说「把这个任务退回去」→ `set --id <任务号> --release`。
|
|
63
73
|
|
|
64
74
|
### log — 记一条进展
|
|
65
75
|
|
|
@@ -91,7 +101,8 @@ allowed-tools: Read,Edit,Bash,Grep
|
|
|
91
101
|
|---|---|
|
|
92
102
|
| `--mine` | 只看自己名下的 |
|
|
93
103
|
| `--pool` | 只看公共池(没人认领的) |
|
|
94
|
-
| `--owner
|
|
104
|
+
| `--owner <uuid>` | 看某个人的(**只收 uuid**,精确匹配;传名字直接报错;没有 `me` 缩写;不能与 `--mine` / `--pool` 同给) |
|
|
105
|
+
| `--owner-name <片段>` | 按显示名模糊筛(子串、不分大小写);**只用于筛选**,不参与任何归属判定 |
|
|
95
106
|
| `--status open\|doing` | 按状态筛(逗号分隔可多选;合法值只有 `open` / `doing`) |
|
|
96
107
|
| `--category` / `--tag` | 按分类(精确)/ 标签(包含)筛 |
|
|
97
108
|
| `--since` / `--until` | 按创建时间筛(只给日期含当天) |
|
|
@@ -116,12 +127,13 @@ allowed-tools: Read,Edit,Bash,Grep
|
|
|
116
127
|
|
|
117
128
|
| 参数 | 含义 |
|
|
118
129
|
|---|---|
|
|
119
|
-
| `--owner
|
|
130
|
+
| `--owner <uuid>` / `--mine` | 按 owner 筛(**只收 uuid**,传名字直接报错);两者**不能同给** |
|
|
131
|
+
| `--owner-name <片段>` | 按显示名模糊筛(子串、不分大小写);只用于筛选 |
|
|
120
132
|
| `--category` / `--tag` | 按分类 / 标签筛 |
|
|
121
133
|
| `--since` / `--until` | 按**结案时间**筛(只给日期含当天) |
|
|
122
134
|
| `--months N` | 最近 N 个自然月(与 `--since` / `--until` **互斥**,同传直接报错) |
|
|
123
135
|
| `--limit N` | 最近 N 条 |
|
|
124
|
-
| `--count-only` | 只输出 `{"count","capped"}`(与 `--json`
|
|
136
|
+
| `--count-only` | 只输出 `{"count","capped"}`(与 `--json`、`--limit` 都互斥) |
|
|
125
137
|
| `--json` | JSON 输出 |
|
|
126
138
|
|
|
127
139
|
干什么:按结案时间倒序排列,每条一行,只列结案时间、负责人、标题,其他都不在表格里;要摘要 / 分类 / 标签 / 完整时间加 `--json`。不给时间参数 = 最近 3 个自然月;最多 13 个月,超了直接报错。
|
|
@@ -145,11 +157,11 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
|
|
|
145
157
|
|
|
146
158
|
1. **建(只建,不认领)—— 默认**:「新建一个任务」「新建个任务,我们来干 X」「新建个任务,我需要处理 Y」→ `new --title "…"`。落公共池、待认领。
|
|
147
159
|
2. **建并认领 —— 仅限明确说法**:用户明确说「新建一个任务**并开始做 / 并执行**」这类带着「现在就开始」的话 → `new --title "…" --self`。直接归自己名下、「进行中」。**除这种明确说法,其他一律按第 1 条只建** —— 别替用户决定他要开始做;拿不定,问一句。
|
|
148
|
-
3. **找已有的**:用户说「我们来做 X」→ 先 `list --mine` 找自己名下的,在做的直接接着做;没有 → `list --pool` 里找,找到先问用户:「X 还没认领,认领它吗?」→ 用户点头再 `claim`;哪儿都没有 →
|
|
160
|
+
3. **找已有的**:用户说「我们来做 X」→ 先 `list --mine` 找自己名下的,在做的直接接着做;没有 → `list --pool` 里找,找到先问用户:「X 还没认领,认领它吗?」→ 用户点头再 `claim`;哪儿都没有 → **不擅自建**:问「要为 X 建个任务吗?」,同意才按第 1/2 条建,不同意就只干活、不登记。
|
|
149
161
|
4. **认领**:`claim --id <任务号>` —— 认到自己名下,状态即「进行中」。
|
|
150
162
|
5. **推进**:改 worklog 前五节 → 按「过程文档」节的收工三步记 `log`。卡住也 `log`(写清卡在哪,状态不动);「先放一放」同理 —— 是暂停,不是释放。
|
|
151
163
|
6. **交付待验收**:任务留「进行中」,「待办」写「待用户验收」,「下一步」写清验收看什么;照收工三步记一条 `log --note "已交付待验收:…"`。
|
|
152
|
-
7. **释放**:仅当用户明确说「退回去 / 释放掉」→ `set --id <任务号> --
|
|
164
|
+
7. **释放**:仅当用户明确说「退回去 / 释放掉」→ `set --id <任务号> --release`。任务回公共池,别人可认领。
|
|
153
165
|
8. **结案**:仅当用户明确说「把这个任务结束掉 / close 掉」→ `close --id <任务号> --summary "…"`;说得含糊(「做完了」「验收通过」)先反问「是要结束这个任务吗?」。收尾顺序:**先补完正文 + 记 log,最后才 close** —— 关了就不能再写。
|
|
154
166
|
9. **查询**:在办 `list` / 单条 `show` / 已结案 `history`(默认只近 3 个自然月,更早的要 `--months` / `--since`)。
|
|
155
167
|
|
|
@@ -171,8 +183,8 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
|
|
|
171
183
|
- 「执行记录」节名一个字都别动;老文档缺这一节也不用补。
|
|
172
184
|
- 过程文档找不到时 `log` 只提示不失败;用 `show` 查路径,不要手写。
|
|
173
185
|
- 头部只有创建时间 / 创建人、封卷标记、分类 / 标签行 —— **状态与归属以台账数据为准,别从头部读**。
|
|
174
|
-
- 执行记录行格式 `- 本地时间 · 类型 · @谁 · 说明`,`@谁` 是这次动手的人、不是任务现归属;只追加、不改旧行。行首「类型」是脚本内部动作名(会见到 `release`,它就是 `set --
|
|
175
|
-
- 只改某条任务的正文:`show --id <任务号>` 拿路径直接改,不另建 md;已结案的用 `history --months 12
|
|
186
|
+
- 执行记录行格式 `- 本地时间 · 类型 · @谁 · 说明`,`@谁` 是这次动手的人、不是任务现归属;只追加、不改旧行。行首「类型」是脚本内部动作名(会见到 `release`,它就是 `set --release`);认不出就别当成新命令,按 `--help` 查。
|
|
187
|
+
- 只改某条任务的正文:`show --id <任务号>` 拿路径直接改,不另建 md;已结案的用 `history --months 12 --json` 找回链接(**人话输出里没有链接,必须加 `--json`**)——默认只查近 3 个月,结案久了的要显式放大窗口;只读不改。
|
|
176
188
|
|
|
177
189
|
### 收工三步(「收工」= 这一轮对用户有可见产出)
|
|
178
190
|
|
|
@@ -186,18 +198,19 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
|
|
|
186
198
|
|
|
187
199
|
### 数据完整性(脚本管不了的,违反了数据就脏)
|
|
188
200
|
|
|
189
|
-
- **执行记录禁手改**:只由脚本按 `new / claim / set / release / log / close`
|
|
201
|
+
- **执行记录禁手改**:只由脚本按 `new / claim / set / release / log / close` 追加,只增不改(**交接记的是 `set`**,看不到 `handoff` 是正常的)。
|
|
190
202
|
- **改正文必配一条日志**:动了 worklog 的正文(改一句、补一节、改标题、修错字都算)就必须立刻跑一次 `log`。
|
|
191
|
-
- **换人只能「释放 → 认领」**:`set --
|
|
192
|
-
-
|
|
203
|
+
- **换人只能「释放 → 认领」**:`set --release` 放回公共池,对方自己 `claim`;不许替别人挂名字。`set --owner <别人>`(预留转接通道,agent 一律不用)。
|
|
204
|
+
- **身份由脚本从本机 identity 文件自取,agent 全程不传身份**:没初始化时先让用户跑 `init`(一台机器一次);查自己用 `--mine`,查别人才传**对方 uuid**(`list --owner <uuid>`);释放用 `--release`,不传身份。
|
|
205
|
+
- **身份只用 uuid,名字只用于显示与筛选**:认领 / 释放 / 交接 / 查「我的」一律只比 uuid;`--owner` 传名字会当场报错,要按人模糊找就用 `--owner-name <片段>`(子串、不分大小写)。名字是冗余字段(改名不影响归属),别拿它当身份用。
|
|
193
206
|
|
|
194
207
|
### 人类授权(跑之前问不问)
|
|
195
208
|
|
|
196
|
-
项目没有 `ledger/` ⇒ 先问「要不要建个台账记这活?」,不许自建。用户同意 ⇒ 直接跑第一条写命令,且**必须带 `--repo <工作区根>`**(根是直接含有 `ledger/` 的那个目录,`ledger/` 本身不是根、指到它会被拒绝):不带时脚本只从当前目录逐级往上找第一个含 `ledger/` 的目录,一个都没有就退 1
|
|
209
|
+
项目没有 `ledger/` ⇒ 先问「要不要建个台账记这活?」,不许自建。用户同意 ⇒ 直接跑第一条写命令,且**必须带 `--repo <工作区根>`**(根是直接含有 `ledger/` 的那个目录,`ledger/` 本身不是根、指到它会被拒绝):不带时脚本只从当前目录逐级往上找第一个含 `ledger/` 的目录,一个都没有就退 1 报「找不到台账」,绝不就地建;台账由这条写命令顺带建出来;但**必须先 `init` 过一次**(一台机器一次,写本机身份,与台账无关)——没 init 任何命令都会报「这台机器还没有初始化身份」。之后可以不带 `--repo`,但查找是**逐级往上**的 —— 在本项目的子目录里跑可能命中上层项目的台账,拿不准就一直带 `--repo <工作区根>`。
|
|
197
210
|
|
|
198
211
|
| 命令 | 档位 | 要求 |
|
|
199
212
|
|---|---|---|
|
|
200
|
-
| `new` / `claim` / `set` | 动手类 |
|
|
213
|
+
| `new` / `claim` / `set` | 动手类 | 执行前复述完整参数并取得确认;**`new` 仅当用户原话点名要建且标题可直取原话时免复述**,标题需另拟或拿不准仍复述一次(`claim`/`set` 一律复述) |
|
|
201
214
|
| `log` | 免确认 | 本任务的推进 / 卡点 / 交付记录直接跑,跑完一句话交代「已记执行记录:…」;要替用户下结论,先问(任务不在自己名下会直接被拒) |
|
|
202
215
|
| `close` | 独立授权 | 仅认明确说「结束掉 / close 掉」(完整判定见生命周期第 8 条);说得含糊必须反问「是要结束这个任务吗?」;**agent 自判「干完了」不算** |
|
|
203
216
|
| `list` / `show` / `history` / `validate` | 只读 | 直接跑 |
|
|
@@ -215,5 +228,5 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
|
|
|
215
228
|
| 何时读 | 文件 |
|
|
216
229
|
|---|---|
|
|
217
230
|
| 任何非 0 退出、退出码 0 但有 `提示:` / `错误:` / `警告:`、并发冲突、`validate` 报错 | `references/troubleshooting.md` |
|
|
218
|
-
| 传 `--created` / `--finished`
|
|
231
|
+
| 传 `--created` / `--finished` 补录时间、看 `--json` 里的时间字段、查别人时 owner 匹配异常、看 history 列义 / 时间窗口口径 | `references/state-model.md` |
|
|
219
232
|
| 拿不准一条记录够不够格、`--note` 不会写 | `references/log-note.md` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-ledger",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Task-ledger skill for coding agents: per-workspace progress tracking (ledger/pool.json + owners/<hash>.json shards + owners.json index, history, worklog) driven by a CLI script. Standalone install for CodeBuddy and other agents; also bundled inside the dsh-task-ledger DSH plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"skill",
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
new → 公共池(open,没人认领)
|
|
9
9
|
new --self → 自己名下(doing,记 started)
|
|
10
10
|
公共池 ──claim──▶ 自己名下(doing;started 空则补 now)
|
|
11
|
-
set --
|
|
11
|
+
set --release → 公共池(open,没人认领;started 保留)
|
|
12
12
|
set --owner 别人 → 对方名下(预留转接通道,agent 不用)
|
|
13
13
|
close → 历史层 + 封卷 + 移出自己名下
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
- **`started` 是外卖计时器**:首次认领盖上之后,释放(`set --
|
|
17
|
-
-
|
|
16
|
+
- **`started` 是外卖计时器**:首次认领盖上之后,释放(`set --release`)不清、重新认领不刷新 —— 逾期时钟从首次认领一口气跑到结案,转手不重置。防的是「眼看要超时,释放掉再认领回来洗时钟」;全链路没有重置 `started` 的命令(`claim` 的 `or now()` 只在它为空时才盖值;`set` 原样保留)。接手一个已超时的任务,一接单即逾期:单子已经迟了,谁端着谁负责;责任归属看执行记录流水(谁认领 / 谁释放逐笔有记录),计时器只回答「这单迟没迟」。
|
|
17
|
+
- 状态只有 `open` / `doing` 两种在办值;`done` 由 `close` 产生(任务从分片消失,只留在历史里)。**没有 `blocked`**,也没有改状态的命令。
|
|
18
18
|
- 认领 / 释放 / 交接一律先写目标、再删原处,崩在中间会同一任务号短暂出现在两处 —— 重跑一次同一条命令即幂等收敛(`already_done`),或按 `validate` 的提示删掉多余那份。
|
|
19
19
|
|
|
20
20
|
## 时间
|
|
@@ -26,6 +26,7 @@ close → 历史层 + 封卷 + 移出自己名下
|
|
|
26
26
|
- **筛选语义**:`list` 的 `--since` / `--until` 按**创建时间**;`history` 的按**结案时间**(历史文件按结案月归档,按创建时间比较会漏「上月建、本月结」的记录)。只给日期时起 = 当天 `00:00:00`、止 = 当天 `23:59:59`(含当天)。
|
|
27
27
|
- **时区**:存盘为 UTC 的只有 —— 任务号、worklog 目录 / 文件名、台账 JSON 的 `created`/`started`/`updated`/`finished`、history 行、worklog 头部「创建」行。**执行记录行、封卷行、`list` / `history` 的人话输出都是本机本地时间**(给人看的日志行);`--json` 原样给 UTC,由消费方自己转。存量老数据(2026-10-22 前)写的是写入者本地时间、无标记,无法与 UTC 区分。
|
|
28
28
|
- **history 文件 9 列**:结案时间 / 创建时间 / 工作包(= `new --title` 的任务标题)/ 分类 / 标签 / owner / creator / 摘要 / 过程文档;排序与 `--since` / `--until` 按**结案时间**。(`history` 命令的人话输出只展示其中几列:结案时间、负责人、标题。)
|
|
29
|
+
- **owner / creator 两列存的是 uuid**(与任务里一致,身份只认 uuid);人话输出与 `--json` 里的 `owner_name` / `creator_name` 是读的时候按 `owners.json` 名册现查补上的显示名,不落盘。名册里查不到就退回裸 uuid。
|
|
29
30
|
|
|
30
31
|
## 任务号
|
|
31
32
|
|
|
@@ -33,10 +34,11 @@ close → 历史层 + 封卷 + 移出自己名下
|
|
|
33
34
|
|
|
34
35
|
## 身份
|
|
35
36
|
|
|
36
|
-
- 主键 =
|
|
37
|
-
-
|
|
38
|
-
- 全站只认
|
|
39
|
-
- `
|
|
37
|
+
- 主键 = **本机 UUID**(`~/.taskledger/identity.json` 里的 `uuid`),归一化后比对;`name` 只用于显示与 `--owner-name` 模糊筛。
|
|
38
|
+
- 没有 `identity.json`(未初始化)**直接报错**,提示先跑 `init`(一台机器一次);不再有兜底身份,也不读 git / 环境变量。
|
|
39
|
+
- 全站只认 UUID 这一个身份键;`list --owner` / `history --owner` **只收 uuid**(形态不是 UUID 直接报错,不静默返回空),没有 `me` 缩写,「自己」用 `--mine`。
|
|
40
|
+
- **名字不进任何判定**:按人模糊找只能走 `--owner-name <片段>`(对名册里的 `name` 做子串、不分大小写匹配,换成 uuid 集合再筛),它只是查询便利,认领 / 释放 / 交接 / 「我的」一律只比 uuid。任务上的 `owner_name` 是冗余显示字段,改名不影响归属。
|
|
41
|
+
- `close` / `log` 只对自己名下的任务生效,不在名下直接拒绝;`set --release` 对已在公共池里的任务是幂等空转(退 0);`set --owner <自己>` 会把公共池里的任务认领到自己名下。
|
|
40
42
|
|
|
41
43
|
## 历史层
|
|
42
44
|
|
|
@@ -20,7 +20,15 @@
|
|
|
20
20
|
|
|
21
21
|
### 用法 / 参数类(退 1,改参数即可)
|
|
22
22
|
|
|
23
|
-
- **「set
|
|
23
|
+
- **「set 必须二选一:--release(释放回公共池)或 --owner <对方 uuid>(交接)」**:`set` 只认这两个,没有别的用法。
|
|
24
|
+
- **「--release 与 --owner 不能同时给」**:释放不带人、交接不带释放,分开跑两次。
|
|
25
|
+
- **「--owner 只收身份 uuid(形如 …),不接受名字 / 邮箱:X」**:身份参数传了名字。改传 uuid;要按显示名找人就加到查询里:`list --owner-name <片段>` / `history --owner-name <片段>`。
|
|
26
|
+
- **「--owner 不能和 --mine / --pool 一起用」**:范围只能选一种。要按某个人筛就 `--owner <uuid>`,要看自己的就 `--mine`。
|
|
27
|
+
- **「--count-only 与 --limit 不能同时用」/「--limit 必须 ≥ 1」**:计数不截断、列表才截断;`--limit 0` 没有意义。
|
|
28
|
+
- **「时间区间反了:--since(…)晚于 --until(…)」**:两端换过来。
|
|
29
|
+
- **「认领目标不是 uuid:X」**:`init --claim` 抄了老台账的邮箱 / 名字键。老台账先迁到 uuid 身份;这台机器先 `init --new --name=<名字>`。
|
|
30
|
+
- **「名册里没有 uuid=X」**:`init --claim` 抄了一个不在 `owners.json` 里的 uuid。不带参数跑一次 `init` 看可选用户再选。
|
|
31
|
+
- **「任务 X 的负责人是 Y,不是你(Z)」/「身上还挂着 owner / 不是 open —— 台账脏了」**:分片键与行内 `owner` 打架(`validate` 的 problem ⑤)。跑 `validate` 看清单、按人工裁决修,别用命令硬覆盖。
|
|
24
32
|
- **「任务号 X 不合法:只能用小写字母、数字、汉字和连字符」**:`--id` 传了大写 / 空格 / 符号。
|
|
25
33
|
- **「任务号 X 已经存在了(这不是重跑,是重号)」**:换个 `--id`,或不传让脚本自动生成。
|
|
26
34
|
- **「标题不能为空」/「--note 不能为空」/「结案要写一句「完成了什么」(--summary 不能为空)」**:必填项漏了。
|
|
@@ -35,9 +43,9 @@
|
|
|
35
43
|
- **「任务 X 不在我的分片里,也没在公共池里」**:任务号打错,或任务已被别人认领。
|
|
36
44
|
- **「任务 X 已在 Y 的分片里(别人正在做),不能认领」/「任务 X 的负责人是 Y,不是你(Z)」**:动别人名下的任务一律拒绝。
|
|
37
45
|
- **「公共池里没有任务 X(已经被人认领或不存在)」**:`claim` 扑空。
|
|
38
|
-
- **「Y 还没在索引里登记过 ⇒ 不能交接给他」**:`set --owner
|
|
46
|
+
- **「Y 还没在索引里登记过 ⇒ 不能交接给他」**:`set --owner <uuid>` 指向一个没在 `owners.json` 登记过的身份(对方从没成功写过台账)。新同事用 `init --new` 建的身份要**先写一条自己的任务**才会被登记(`init` 只写机器身份文件、不碰台账);用 `init --claim` 认领老 uuid 的人一开始就在名册里。
|
|
39
47
|
- **「任务 X 在 Y 的分片里已经存在,且内容与本次不一致 ⇒ 拒绝交接」**:对方名下已有一份不一样的同名任务,交不过去。
|
|
40
|
-
- **换人只能「对方释放 → 你认领」**:`set --
|
|
48
|
+
- **换人只能「对方释放 → 你认领」**:`set --release` 放回公共池,对方自己 `claim`;不要绕过脚本改 JSON。
|
|
41
49
|
|
|
42
50
|
### 台账本身有问题(退 1 / 退 3)
|
|
43
51
|
|
|
@@ -79,14 +87,13 @@
|
|
|
79
87
|
|
|
80
88
|
### 只提示、不拦(退出码 0)
|
|
81
89
|
|
|
82
|
-
-
|
|
90
|
+
- **「错误:这台机器还没有初始化身份」**:先跑 `init`(一台机器一次),输入名字;没初始化任何命令都不执行。
|
|
83
91
|
- **「提示:格式版本是 … 当前是 …;不拦,但字段可能有出入」**:数据是旧版本写的,功能照常;想统一就按提示升版本。
|
|
84
92
|
- **「提示:写前把本地快进到 …(N 个提交,仅 fast-forward)」**:纯告知。
|
|
85
93
|
- **「提示:写前快进到 … 没成功(工作树有未提交改动),改为只刷新本次要写的台账文件」**:没碍事,照写。
|
|
86
94
|
- **「提示:rebase 成功,但把你的未提交改动放回工作树时冲突了」**:冲突标记只在被点名的文件里,解完 `git add` 后重跑台账命令即可。
|
|
87
95
|
- **「提示:只写了本地文件、未签入 git(也未推送)」**:用了 `--no-sync`;之后自己 `git add ledger && commit && push`。
|
|
88
96
|
- **「提示:当前目录不是 git 仓库,台账只写了本地文件」**:没有 git 也能用;想纳入版本管理就 `git init`,否则继续本地写(多人各写各的,后写覆盖先写)。
|
|
89
|
-
- **「警告:历史表列数不是 9 且无法整体升级,本次按现有列数追加」**:历史文件是旧列数,本次照旧格式追加;要统一就手工升级表头(9 列见 state-model)。
|
|
90
97
|
- **「提示:任务 X 没有过程文档,已跳过封卷,历史末列留空」**:老数据没有 worklog 字段,结案照常,只是没有文档可封。
|
|
91
98
|
- **`无需重复:这次没有新的改动。`**(`claim` / `set` / `close` 的 already_done 通道):上次已经做成过,重跑是幂等空转;`close` 重跑还会顺手补齐上次崩在半途的封卷与移出。
|
|
92
99
|
- **`validate` 的 notices**(同号任务出现在多个文件、历史解析警告、worklog 仍是骨架等):只提示不判错,照实转述。
|
|
@@ -96,9 +103,14 @@
|
|
|
96
103
|
两档:默认**浅度**只查格式与结构(账本 JSON + 当月历史 + 每篇过程文档的模板格式);加 **`--deep`** 才多读全部历史月份、并读正文判「还是不是空骨架」。
|
|
97
104
|
|
|
98
105
|
problems(任一即退 1):① 池里任务带着 owner;② 池里任务 status 不是 open/doing;③ 索引登记了身份但分片文件不在;④ 分片任务没有 owner;⑤ 分片任务 owner 与分片所属键不一致;⑥ 分片任务 status 非法;⑦ `owners/` 里有索引没登记的 `*.json`;⑧ 过程文档格式坏(第一行不是标题 / 头部缺「创建」行 / 前五节缺或顺序不对)。
|
|
99
|
-
notices
|
|
106
|
+
notices(只提示):过程文档缺「执行记录」节或有重复的节、历史解析警告、历史有结案行但任务还在办、还没有 `owners.json`;`--deep` 另加「过程文档还是空骨架」。
|
|
100
107
|
①~⑦ 里结构类的属人工裁决:读原文确认哪份是真的,删多余那份或把索引改对,**不要用脚本猜**。索引或任一 JSON 损坏 ⇒ 退 3。
|
|
101
108
|
|
|
109
|
+
## 读命令只读本地(跨机器要先取最新)
|
|
110
|
+
|
|
111
|
+
- **读命令(`list` / `show` / `history` / `validate`)不会自动 `git pull`**:看到的是**这台机器上一次同步后的**台账。
|
|
112
|
+
- 想看别的机器刚写的东西:先 `git pull`,或者跑任意一条写命令(写命令进入时会先取最新)。
|
|
113
|
+
|
|
102
114
|
## 写前取最新与写后同步(机制)
|
|
103
115
|
|
|
104
116
|
- 写命令全程持仓级锁(git 公共目录里的 `dsh-task-ledger.sync.lock`,同仓库所有 worktree 共享,最多等 60 秒;`--no-sync` 也跳不过这把锁)。只读命令不排队。
|
|
@@ -106,7 +118,7 @@ notices(只提示):过程文档缺「执行记录」节或有重复的节
|
|
|
106
118
|
- **写后**:只 `git add` 台账路径,然后 `commit --no-verify` —— 提交失败退 1;push 失败只提示、退 0,被拒时自动 `rebase --autostash` 合并一次:不同任务自动合并,同一任务两边都改留冲突标记让人定;本地有非台账提交时不 rebase。
|
|
107
119
|
- **回滚**:命令失败时,写前刷下来的内容按字节还原、本次新建的文件删掉;业务写的自补偿只有两处 —— `new`(删任务行 + 删文档)、`close`(三个文件按字节还原)。
|
|
108
120
|
- **`--no-sync` / `TASK_LEDGER_NO_SYNC=1`**:跳过 guard、取最新、提交与推送;自举照跑;有写入时会提示「只写了本地文件、未签入 git」。注意它挂在**顶层**:`progress.py --no-sync <子命令> …`,放子命令后面是用法错。
|
|
109
|
-
- **环境**:git ≥ 2.27(自动合并依赖 `rebase --autostash`)。不是 git 仓库 ⇒ 同步整体跳过、台账照写,并提示「只写了本地文件」;连 git
|
|
121
|
+
- **环境**:git ≥ 2.27(自动合并依赖 `rebase --autostash`)。不是 git 仓库 ⇒ 同步整体跳过、台账照写,并提示「只写了本地文件」;连 git 二进制都没装也照样写(身份只读本机 identity 文件,与 git 无关)。
|
|
110
122
|
|
|
111
123
|
## 硬约束
|
|
112
124
|
|
|
@@ -61,13 +61,13 @@ def _pool(repo: Path) -> Path:
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def _shard(repo: Path, me: Identity) -> Path:
|
|
64
|
-
return shard_rel(me.
|
|
64
|
+
return shard_rel(me.uuid)
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def _other_shards(repo: Path, me: Identity) -> tuple[Path, ...]:
|
|
68
68
|
"""索引里除我之外的分片(宁宽:认领的前置要查、交接要写对方那份)。"""
|
|
69
69
|
index = OwnersIndex(repo)
|
|
70
|
-
me_key = normalize(me.
|
|
70
|
+
me_key = normalize(me.uuid)
|
|
71
71
|
out: list[Path] = []
|
|
72
72
|
for key, record in index.all().items():
|
|
73
73
|
if key == me_key:
|
|
@@ -17,14 +17,21 @@ from ..store.task_store import TaskStore
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def bootstrap(repo: Path, me: Identity) -> Identity:
|
|
20
|
-
"""写序:`owners/` 目录 → 池 →
|
|
20
|
+
"""写序:`owners/` 目录 → 池 → **我的分片 → 索引(登记自己)**。已登记 ⇒ 零写。
|
|
21
|
+
|
|
22
|
+
分片必须排在登记之**前**:反过来的话,硬崩(SIGKILL / 断电)正好落在两步之间,
|
|
23
|
+
会留下「索引里有我、磁盘上没有我的分片」——此后**所有人**的 list / show 都在
|
|
24
|
+
`assert_present` 处报「登记的任务文件找不到了」,而 `ensure` 看到记录已存在就零写,
|
|
25
|
+
这个坏状态永远不自愈。按现在的顺序崩,最多留一份孤儿分片(`validate` 会报出来),
|
|
26
|
+
下一条写命令 bootstrap 时会把它登记回去。
|
|
27
|
+
"""
|
|
21
28
|
repo = as_root(
|
|
22
29
|
repo, source="工作区根"
|
|
23
30
|
) # 门 3:写前最后确认,绝不在 ledger/ 里再套一层
|
|
24
31
|
ensure_dir(repo / LEDGER_DIR) # 曾是 to_repo_rel(LEDGER_DIR) ⇒ 会拼出 ledger/ledger
|
|
25
32
|
ensure_dir(repo / to_repo_rel(OWNERS_DIR))
|
|
26
33
|
TaskStore(repo, POOL_REL).ensure_file()
|
|
34
|
+
TaskStore(repo, shard_rel(me.uuid), identity=me).ensure_file()
|
|
27
35
|
index = OwnersIndex(repo)
|
|
28
36
|
_, _ = index.ensure(me) # 登记自己(已登记则零写)
|
|
29
|
-
TaskStore(repo, shard_rel(me.email), identity=me).ensure_file()
|
|
30
37
|
return me
|
|
@@ -18,8 +18,8 @@ from .result import Result, rel
|
|
|
18
18
|
def claim(repo: Path, me: Identity, tid: str) -> Result:
|
|
19
19
|
"""写序:我的分片 → 删池里那行 → 过程文档记一笔。"""
|
|
20
20
|
pool = TaskStore(repo, POOL_REL)
|
|
21
|
-
mine = TaskStore(repo, shard_rel(me.
|
|
22
|
-
me_key = normalize(me.
|
|
21
|
+
mine = TaskStore(repo, shard_rel(me.uuid), identity=me)
|
|
22
|
+
me_key = normalize(me.uuid)
|
|
23
23
|
|
|
24
24
|
# 前置:别人正在改它(他的分片里已有这个号)⇒ 拒绝
|
|
25
25
|
for key, path in OwnersIndex(repo).shards().items():
|
|
@@ -40,7 +40,7 @@ def claim(repo: Path, me: Identity, tid: str) -> Result:
|
|
|
40
40
|
if path is not None:
|
|
41
41
|
logs.append_exec(
|
|
42
42
|
path,
|
|
43
|
-
exec_line(now(), me.
|
|
43
|
+
exec_line(now(), me.uuid, "claim", "认领(补齐上次未完成的清理)"),
|
|
44
44
|
)
|
|
45
45
|
return Result(
|
|
46
46
|
already_done=True,
|
|
@@ -51,14 +51,15 @@ def claim(repo: Path, me: Identity, tid: str) -> Result:
|
|
|
51
51
|
raise Fail(f"公共池里没有任务 {tid}(已经被人认领或不存在)。")
|
|
52
52
|
|
|
53
53
|
current = task.get("status")
|
|
54
|
-
if current
|
|
54
|
+
if current != "open":
|
|
55
55
|
raise Fail(
|
|
56
56
|
f"任务 {tid} 在公共池里但状态是 {current}(应为 open)—— "
|
|
57
57
|
"先跑 validate 看台账是不是乱了。"
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
moved = dict(task)
|
|
61
|
-
moved["owner"] = me.
|
|
61
|
+
moved["owner"] = me.uuid
|
|
62
|
+
moved["owner_name"] = me.name
|
|
62
63
|
# 认领即在做:看板把 doing 当「我的在办」算(home.service.ts),置 open 会让
|
|
63
64
|
# 认领后的任务看起来仍没人领。旧版 progress.py 置 doing,分片重构时丢了。
|
|
64
65
|
moved["status"] = "doing"
|
|
@@ -71,7 +72,7 @@ def claim(repo: Path, me: Identity, tid: str) -> Result:
|
|
|
71
72
|
logs = WorklogStore(repo)
|
|
72
73
|
path = logs.find(tid, moved.get("worklog"))
|
|
73
74
|
if path is not None:
|
|
74
|
-
logs.append_exec(path, exec_line(now(), me.
|
|
75
|
+
logs.append_exec(path, exec_line(now(), me.label(), "claim", "认领 → doing"))
|
|
75
76
|
note = None
|
|
76
77
|
return Result(
|
|
77
78
|
changed=("owner", "started", "updated"),
|
|
@@ -23,10 +23,9 @@ from ..store.atomic import ensure_dir, fingerprint, restore, snapshot
|
|
|
23
23
|
from ..store.history_store import HIT, UNDECIDABLE, HistoryStore
|
|
24
24
|
from ..store.task_store import TaskStore
|
|
25
25
|
from ..store.worklog_store import WorklogStore
|
|
26
|
+
from .guard import require_owner
|
|
26
27
|
from .result import Result, rel
|
|
27
28
|
|
|
28
|
-
_COLS_FALLBACK_HINT = "警告:历史表列数不是 9 且无法整体升级,本次按现有列数追加。"
|
|
29
|
-
|
|
30
29
|
|
|
31
30
|
def close_task(
|
|
32
31
|
repo: Path,
|
|
@@ -51,7 +50,7 @@ def close_task(
|
|
|
51
50
|
history = HistoryStore(repo)
|
|
52
51
|
hist_path = repo / to_repo_rel(history_path_for(finished))
|
|
53
52
|
|
|
54
|
-
mine = TaskStore(repo, shard_rel(me.
|
|
53
|
+
mine = TaskStore(repo, shard_rel(me.uuid), identity=me)
|
|
55
54
|
task = mine.get(tid) # 锁内重读
|
|
56
55
|
if task is None:
|
|
57
56
|
# 分片里已经没有这条:上次崩在「删分片」之后。只按过程文档名判重,命中即已结案
|
|
@@ -66,6 +65,9 @@ def close_task(
|
|
|
66
65
|
)
|
|
67
66
|
raise Fail(f"任务 {tid} 不在我的分片里(只能结案自己的任务)。")
|
|
68
67
|
|
|
68
|
+
# 分片里在、但行内 owner 不是我 ⇒ 台账脏了(validate 会报⑤):先拦下来,别用我的 uuid 记历史
|
|
69
|
+
require_owner(task, tid, me)
|
|
70
|
+
|
|
69
71
|
# 历史行那两列取**最终值**:给了参数用新值,没给用任务原值,两处必须一致
|
|
70
72
|
final_category = new_category if new_category else task.get("category")
|
|
71
73
|
final_tags = tuple(new_tags) if new_tags else tuple(task.get("tags") or ())
|
|
@@ -93,9 +95,9 @@ def close_task(
|
|
|
93
95
|
if doc_path is not None:
|
|
94
96
|
text = logs.read(doc_path)
|
|
95
97
|
if not has_frozen_line(text):
|
|
96
|
-
frozen = stamp_frozen(text, finished, me.
|
|
98
|
+
frozen = stamp_frozen(text, finished, me.label())
|
|
97
99
|
frozen = append_exec_log(
|
|
98
|
-
frozen, exec_line(finished, me.
|
|
100
|
+
frozen, exec_line(finished, me.label(), "close", "结案封卷")
|
|
99
101
|
)
|
|
100
102
|
logs.write(doc_path, frozen)
|
|
101
103
|
notes.append("补齐上次未完成的封卷。")
|
|
@@ -136,8 +138,10 @@ def close_task(
|
|
|
136
138
|
"title": task.get("title") or "",
|
|
137
139
|
"category": final_category,
|
|
138
140
|
"tags": final_tags,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
# 历史两列只存 **uuid**(身份唯一键);显示名在读的时候按名册换,
|
|
142
|
+
# 不落盘 —— 否则 `history --owner <uuid>` / `--mine` 永远比不到东西。
|
|
143
|
+
"owner": me.uuid,
|
|
144
|
+
"creator": task.get("creator") or me.uuid,
|
|
141
145
|
"summary": text_summary,
|
|
142
146
|
"worklog": task.get("worklog") if doc_path else None,
|
|
143
147
|
},
|
|
@@ -148,9 +152,9 @@ def close_task(
|
|
|
148
152
|
if doc_path is not None:
|
|
149
153
|
text = logs.read(doc_path)
|
|
150
154
|
if not has_frozen_line(text):
|
|
151
|
-
frozen = stamp_frozen(text, finished, me.
|
|
155
|
+
frozen = stamp_frozen(text, finished, me.label())
|
|
152
156
|
frozen = append_exec_log(
|
|
153
|
-
frozen, exec_line(finished, me.
|
|
157
|
+
frozen, exec_line(finished, me.label(), "close", "结案封卷")
|
|
154
158
|
)
|
|
155
159
|
logs.write(doc_path, frozen)
|
|
156
160
|
|
|
@@ -163,8 +167,6 @@ def close_task(
|
|
|
163
167
|
raise
|
|
164
168
|
|
|
165
169
|
notices: list[str] = []
|
|
166
|
-
if out.get("fallback_cols"):
|
|
167
|
-
notices.append(_COLS_FALLBACK_HINT)
|
|
168
170
|
if doc_path is None:
|
|
169
171
|
notices.append(f"提示:任务 {tid} 没有过程文档,已跳过封卷,历史末列留空。")
|
|
170
172
|
return Result(
|
|
@@ -174,7 +176,6 @@ def close_task(
|
|
|
174
176
|
payload={
|
|
175
177
|
"history_path": rel(repo, hist_path),
|
|
176
178
|
"worklog_path": rel(repo, doc_path) if doc_path else None,
|
|
177
|
-
"fallback_cols": out.get("fallback_cols"),
|
|
178
179
|
},
|
|
179
180
|
notices=tuple(notices),
|
|
180
181
|
)
|
|
@@ -10,8 +10,7 @@ from collections.abc import Mapping
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
-
from ..core.identity import Identity, is_me, normalize
|
|
14
|
-
from ..core.labels import LEGACY_STATUSES
|
|
13
|
+
from ..core.identity import Identity, is_me, looks_like_uuid, normalize
|
|
15
14
|
from ..core.paths import POOL_REL
|
|
16
15
|
from ..errors import Fail
|
|
17
16
|
from ..store.owners_index import OwnersIndex
|
|
@@ -20,12 +19,29 @@ from .query import ListQuery
|
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def current_identity(repo: Path) -> Identity:
|
|
23
|
-
"""env
|
|
22
|
+
"""env 的本机身份(不写盘、不登记)。全站只认 uuid 这一个身份键。"""
|
|
24
23
|
from .. import env
|
|
25
24
|
|
|
26
25
|
return env.current(repo)
|
|
27
26
|
|
|
28
27
|
|
|
28
|
+
def require_uuid(value: str, *, field: str) -> str:
|
|
29
|
+
"""身份参数只收 UUID:名字 / 邮箱一律当场报错,绝不悄悄比不到东西。
|
|
30
|
+
|
|
31
|
+
名字只能用于查询筛选(`--owner-name`),这一点要在报错里说清楚,
|
|
32
|
+
否则用户会以为「名字也行,只是没查到」。
|
|
33
|
+
"""
|
|
34
|
+
text = normalize(value)
|
|
35
|
+
if not looks_like_uuid(text):
|
|
36
|
+
raise Fail(
|
|
37
|
+
f"{field} 只收身份 uuid(形如 00000000-0000-4000-8000-000000000000),"
|
|
38
|
+
f"不接受名字 / 邮箱:{value.strip()}\n"
|
|
39
|
+
" 按显示名模糊筛请用 --owner-name <片段>;"
|
|
40
|
+
"不知道对方 uuid 就先跑 list --json 看,或让本人跑 list --mine。"
|
|
41
|
+
)
|
|
42
|
+
return text
|
|
43
|
+
|
|
44
|
+
|
|
29
45
|
def require_task(tasks: Mapping[str, Any], tid: str, *, where: Path) -> dict[str, Any]:
|
|
30
46
|
"""取一条任务;缺失时给可读原因(带所在文件)。"""
|
|
31
47
|
task = tasks.get(tid)
|
|
@@ -59,34 +75,40 @@ def owner_of(task: Mapping[str, Any]) -> str | None:
|
|
|
59
75
|
|
|
60
76
|
|
|
61
77
|
def status_of(task: Mapping[str, Any]) -> str:
|
|
62
|
-
"""读 `status`
|
|
78
|
+
"""读 `status` 的唯一入口。"""
|
|
63
79
|
value = task.get("status")
|
|
64
80
|
if not isinstance(value, str) or not value.strip():
|
|
65
81
|
return "open"
|
|
66
|
-
|
|
67
|
-
return "doing" if value in LEGACY_STATUSES else value
|
|
82
|
+
return value.strip().lower()
|
|
68
83
|
|
|
69
84
|
|
|
70
85
|
def is_mine(task: Mapping[str, Any], me: Identity) -> bool:
|
|
71
|
-
"""
|
|
86
|
+
"""这条任务是不是我的(只比 uuid)。"""
|
|
72
87
|
return is_me(me, owner_of(task) or "")
|
|
73
88
|
|
|
74
89
|
|
|
75
|
-
def in_scope(
|
|
76
|
-
|
|
90
|
+
def in_scope(
|
|
91
|
+
task: Mapping[str, Any],
|
|
92
|
+
q: ListQuery,
|
|
93
|
+
*,
|
|
94
|
+
me: Identity,
|
|
95
|
+
name_keys: set[str] | frozenset[str] = frozenset(),
|
|
96
|
+
) -> bool:
|
|
97
|
+
"""`list` 的过滤谓词:范围、状态、分类、标签、创建时间闭区间。
|
|
98
|
+
|
|
99
|
+
`name_keys` 是 `--owner-name` 先在名册里模糊匹配出来的 uuid 集合(由调用方查好传进来):
|
|
100
|
+
**名字只用来缩小 uuid 集合,判定本身仍按 uuid 比。**
|
|
101
|
+
"""
|
|
77
102
|
if q.scope == "mine" and not is_mine(task, me):
|
|
78
103
|
return False
|
|
79
104
|
if q.scope == "pool" and owner_of(task):
|
|
80
105
|
return False
|
|
81
106
|
if q.scope == "owner" and q.owner and normalize(q.owner) != owner_of(task):
|
|
82
107
|
return False
|
|
83
|
-
if q.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"doing" in wanted and status in LEGACY_STATUSES
|
|
88
|
-
):
|
|
89
|
-
return False
|
|
108
|
+
if q.owner_name and owner_of(task) not in name_keys:
|
|
109
|
+
return False
|
|
110
|
+
if q.statuses and status_of(task) not in set(q.statuses):
|
|
111
|
+
return False
|
|
90
112
|
if q.category and (task.get("category") or "") != q.category:
|
|
91
113
|
return False
|
|
92
114
|
if q.tag and q.tag not in (task.get("tags") or ()):
|
|
@@ -12,7 +12,7 @@ from ..errors import Fail
|
|
|
12
12
|
from ..store.owners_index import OwnersIndex
|
|
13
13
|
from ..store.task_store import TaskStore
|
|
14
14
|
from ..store.worklog_store import WorklogStore
|
|
15
|
-
from .guard import same_task_ignoring_stamp
|
|
15
|
+
from .guard import owner_of, same_task_ignoring_stamp
|
|
16
16
|
from .result import Result, rel
|
|
17
17
|
|
|
18
18
|
|
|
@@ -24,15 +24,26 @@ def handoff(repo: Path, me: Identity, tid: str, *, to: str) -> Result:
|
|
|
24
24
|
raise Fail(
|
|
25
25
|
f"{to} 还没在索引里登记过 ⇒ 不能交接给他(写出一份对别人不存在的分片,他自己也看不到)。"
|
|
26
26
|
)
|
|
27
|
-
mine = TaskStore(repo, shard_rel(me.
|
|
27
|
+
mine = TaskStore(repo, shard_rel(me.uuid), identity=me)
|
|
28
28
|
task = mine.get(tid) # 锁内重读
|
|
29
29
|
if task is None:
|
|
30
30
|
raise Fail(f"任务 {tid} 不在我的分片里,交接不了。")
|
|
31
31
|
|
|
32
32
|
to_key = normalize(to)
|
|
33
|
+
# 行内 owner 只能是我(正常)或已经是对方(上次交接崩在半途,这次要补齐):
|
|
34
|
+
# 别的一律拒绝——分片里有、owner 却写着第三个人 ⇒ 台账脏了
|
|
35
|
+
owner = owner_of(task)
|
|
36
|
+
if owner not in (normalize(me.uuid), to_key):
|
|
37
|
+
raise Fail(
|
|
38
|
+
f"任务 {tid} 的负责人是 {owner},不是你({me.label()}),交接不了。\n"
|
|
39
|
+
" 处理:跑 validate 看台账是不是脏了。"
|
|
40
|
+
)
|
|
41
|
+
to_name = str(record.get("name") or "") or None # 只用于显示
|
|
42
|
+
who = to_name or to_key
|
|
33
43
|
to_store = TaskStore(repo, shard_rel(to_key))
|
|
34
44
|
moved = dict(task)
|
|
35
45
|
moved["owner"] = to_key
|
|
46
|
+
moved["owner_name"] = to_name
|
|
36
47
|
moved["updated"] = now()
|
|
37
48
|
|
|
38
49
|
existing = to_store.get(tid)
|
|
@@ -47,12 +58,12 @@ def handoff(repo: Path, me: Identity, tid: str, *, to: str) -> Result:
|
|
|
47
58
|
path = logs.find(tid, moved.get("worklog"))
|
|
48
59
|
if path is not None:
|
|
49
60
|
logs.append_exec(
|
|
50
|
-
path, exec_line(now(), me.
|
|
61
|
+
path, exec_line(now(), me.label(), "set", f"交接给 {who}(补齐)")
|
|
51
62
|
)
|
|
52
63
|
return Result(
|
|
53
64
|
already_done=True,
|
|
54
65
|
where=rel(repo, to_store.path),
|
|
55
|
-
message=f"交接 {tid} → {
|
|
66
|
+
message=f"交接 {tid} → {who}:补齐上次未完成的清理",
|
|
56
67
|
notices=(f"任务 {tid} 已在对方分片里(内容一致),本次只补删我分片那行。",),
|
|
57
68
|
)
|
|
58
69
|
|
|
@@ -63,12 +74,12 @@ def handoff(repo: Path, me: Identity, tid: str, *, to: str) -> Result:
|
|
|
63
74
|
path = logs.find(tid, moved.get("worklog"))
|
|
64
75
|
note = None
|
|
65
76
|
if path is None:
|
|
66
|
-
note = f"任务 {tid} 已交给 {
|
|
77
|
+
note = f"任务 {tid} 已交给 {who},但过程文档没找到,跳过执行记录。"
|
|
67
78
|
else:
|
|
68
|
-
logs.append_exec(path, exec_line(now(), me.
|
|
79
|
+
logs.append_exec(path, exec_line(now(), me.label(), "set", f"交接给 {who}"))
|
|
69
80
|
return Result(
|
|
70
|
-
changed=("owner", "updated"),
|
|
81
|
+
changed=("owner", "owner_name", "updated"),
|
|
71
82
|
where=rel(repo, to_store.path),
|
|
72
|
-
message=f"交接 {tid} → {
|
|
83
|
+
message=f"交接 {tid} → {who}",
|
|
73
84
|
notices=(note,) if note else (),
|
|
74
85
|
)
|