task-ledger 1.1.0 → 1.2.1

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 CHANGED
@@ -11,7 +11,7 @@
11
11
  |---|---|---|
12
12
  | 在办任务的状态 | `ledger/progress.json` | **只由脚本写** |
13
13
  | 每个任务的过程正文 | `ledger/worklog/<年月>/<日>-<时分>-<说明>-<随机码>.md` | **建档只由脚本按模板做**,之后正文 **agent 直接编辑** |
14
- | 已结案任务,一行一条(8 列:结案时间 / 创建时间 / 工作包 / 分类 / 标签 / owner / 摘要 / 过程文档) | `ledger/history/<年月>.md`(按**结案时间**分月;存量 `ledger/PROGRESS-HISTORY.md` 只读兼容、永不重写) | **只由脚本写**;查询走 `history` 子命令 |
14
+ | 已结案任务,一行一条(9 列:结案时间 / 创建时间 / 工作包 / 分类 / 标签 / owner / creator / 摘要 / 过程文档) | `ledger/history/<年月>.md`(按**结案时间**分月;存量 `ledger/PROGRESS-HISTORY.md` 只读兼容、永不重写) | **只由脚本写**;查询走 `history` 子命令 |
15
15
 
16
16
  覆盖的动作:建任务、认领、交接、改状态、结案、查在办、查历史、体检。
17
17
 
@@ -35,7 +35,7 @@
35
35
 
36
36
  ### 状态用 JSON,不用 Markdown
37
37
 
38
- 状态要频繁改(`doing` → `blocked` → `done`),Markdown 里改一个词很容易顺手改到别处。JSON 结构化、字段固定、一次只动一个值,改动面最小。
38
+ 状态要频繁改(`open` → `doing` → 结案),Markdown 里改一个词很容易顺手改到别处。JSON 结构化、字段固定、一次只动一个值,改动面最小。
39
39
 
40
40
  ### 状态与过程正文分离
41
41
 
@@ -102,7 +102,7 @@
102
102
 
103
103
  ### 只能改自己的任务
104
104
 
105
- `set` 和 `close` 都先取身份,再校验「这个任务的 owner 是不是你」:**不是就拒绝**;任务还没人认领(`open`)也拒绝,提示先 `claim`。要接手就走 `set --owner` 交接 —— 一次显式动作。
105
+ `set`、`close` 和 `log` 都先取身份,再校验「这个任务的 owner 是不是你」:**不是就拒绝**;任务还没人认领(`open`)也拒绝,提示先 `claim`。要接手就走 `set --owner` 交接 —— 一次显式动作。
106
106
 
107
107
  理由有两层:一是别人替你结案,你会失去对进度的掌控;二是如果只取了身份却不校验归属,`owner` 这个字段就只是个装饰品。
108
108
 
@@ -114,7 +114,7 @@
114
114
 
115
115
  ### 并发策略
116
116
 
117
- - 所有写命令(`init` / `new` / `claim` / `set` / `close`)走同一套:**读 → 改内存 → 复查指纹 → 原子写**
117
+ - 所有写命令(`init` / `new` / `claim` / `set` / `log` / `close`)走同一套:**读 → 改内存 → 复查指纹 → 原子写**
118
118
  - 临时文件名唯一(`mkstemp`),写完 `fsync` 再 `os.replace`,无论成败都清理临时文件
119
119
  - key 按字母序落盘,减少两人同时新增时的文本冲突
120
120
  - 冲突时**报错要求重跑**,不静默覆盖
@@ -171,7 +171,7 @@
171
171
  graph LR
172
172
  U[用户说一句话] --> A[agent 读 SKILL.md 判断意图]
173
173
  A -->|只读直接跑| R[list / show / validate / history]
174
- A -->|先复述参数再确认| W[init / new / claim / set / close]
174
+ A -->|写操作| W[init / new / claim / set / log / close]
175
175
  W --> P[ledger/progress.json]
176
176
  W --> H[ledger/history/年月.md]
177
177
  W --> M[ledger/worklog/年月/日-时分-说明-码.md]
@@ -187,21 +187,19 @@ stateDiagram-v2
187
187
  [*] --> open: new(不带 --self)
188
188
  [*] --> doing: new --self
189
189
  open --> doing: claim(认领,写 owner)
190
- doing --> blocked: set --status blocked
191
- blocked --> doing: set --status doing
192
- doing --> open: set --status open(退回未认领,清空 owner)
193
- blocked --> open: set --status open
190
+ doing --> open: set --status open(释放认领,清空 owner)
194
191
  doing --> [*]: close(进历史)
195
- blocked --> [*]: close(进历史)
196
192
  note right of doing
197
193
  set --owner 交接:owner 换成别人
198
194
  (只有当前 owner 能执行)
195
+ 阻塞 = 接任务满 7 天;停滞 = 最后更新满 3 天
196
+ (都由看板按时间算,不是状态)
199
197
  end note
200
198
  ```
201
199
 
202
200
  - `done` **只能由 `close` 产生**;`set --status done` 会被拒绝
203
201
  - **退回未认领**:owner 用 `set --status open` 把任务退回去,owner 清空、之后别人可以重新 `claim`;这条不能与 `--owner` 同时给
204
- - `set --owner <某人>` 是**交接**,由当前 owner 执行,执行后 owner 换成那个人
202
+ - `set --owner <某人>` 是**交接**,由当前 owner 执行,执行后 owner 换成那个人。**该能力只为将来的 UI 保留**:SKILL.md 不向 agent 暴露它,换人一律走「释放 → 认领」,避免随便编一个名字挂上去
205
203
  - 状态表里不允许残留 `done`(`validate` 会报)
206
204
 
207
205
  ### 命令一览
@@ -211,11 +209,13 @@ stateDiagram-v2
211
209
  | `init` | 建状态表 | 否 | 否 |
212
210
  | `new` | 建任务并建档 | **是**(记 creator) | 是 |
213
211
  | `claim` | 认领待认领任务 | **是** | 是 |
214
- | `set` | 改状态 / 交接 | **是**(且必须是该任务的 owner) | 是 |
212
+ | `set` | 改状态 / 交接 / 释放认领(`--status open`) | **是**(且必须是该任务的 owner) | 是 |
213
+ | `log` | 往 worklog「执行记录」补一条 | **是**(且必须是该任务的 owner) | 否(只回一行确认) |
215
214
  | `close` | 结案归档 | **是**(且必须是该任务的 owner) | 否 |
216
215
  | `list` | 查在办任务 | 仅 `--mine` | 否 |
217
216
  | `history` | 查已结案任务(只读) | 仅 `--mine` | 否(只给过程文档链接) |
218
217
  | `show` | 单任务详情 | 否 | 是 |
218
+ | `tags` | 分类/标签巡检(`--all` 只读)/ 归并(`--rename` 批量写) | 否 | 否 |
219
219
  | `validate` | 校验一致性 | 否 | 否 |
220
220
 
221
221
  参数细节看 `SKILL.md`,或直接跑 `<子命令> --help`。
@@ -225,6 +225,7 @@ stateDiagram-v2
225
225
  | 命令 | 返回 |
226
226
  |---|---|
227
227
  | `new` / `claim` / `set` / `show` | 状态行 + 该任务过程文档的**路径与全文** |
228
+ | `log` | 一行确认(记了什么) |
228
229
  | `close` | 结案结果与摘要 |
229
230
  | `list` | 在办任务列表(不含正文,避免撑爆上下文) |
230
231
  | `history` | 已结案紧凑列表(默认不含摘要,避免撑爆上下文) |
@@ -249,3 +250,9 @@ stateDiagram-v2
249
250
 
250
251
  1. 本 skill 位于工作区 `_Skills/task-ledger/`;是否安装到全局 skill 目录由使用者决定 —— 未安装时其它项目不会自动触发。
251
252
  2. `scripts/progress.py` 里的 `FALLBACK_TEMPLATE` 是**无注释的最小骨架**,只在 `assets/worklog.md` 读不到时启用 —— 两处注释多寡不同不是缺陷,**正例只放模板文件**。
253
+
254
+ ## 五、维护者须知
255
+
256
+ - `SKILL.md` 的 frontmatter `version` 与 `package.json` 的 `version` 保持同值;发版时一起改,打 `skill-v<version>` tag。
257
+ - **`SKILL.md` 是每次会话整份注入的**:只放「不做就会写错数据」的规则与路由,细节进 `references/`(按需读,索引在 SKILL.md 末尾)。加体积前先自问「脚本或 `--help` 能不能兜住」。
258
+ - 改完跑 `cd packages/plugin && npm run verify`(build + typecheck + smoke);`copy-skill` 会把 `scripts/` `assets/` `references/` 一并拷进插件内嵌副本,不要直接改那份拷贝。
package/SKILL.md CHANGED
@@ -1,176 +1,129 @@
1
1
  ---
2
2
  name: task-ledger
3
- description: 管理任务进度(开发任务、个人事务、运维杂事、调研备忘等一切在办的事):新建、认领、交接、改状态、结案、查在办、查历史,状态存于 ledger/progress.json。当用户说「新建一个任务」「建个坑」「开个新活」「立项」「我要做 X」「记一下要做 X」「这事先记着」「这活我领了」「开始做 X」「卡住了」「先放一放」「转给某人」「这活做完了 / 结案」「现在该做什么」「我还有什么没做完」「谁在做这个」「某件老活是什么时候做的」「谁改过什么」「拉某人的工作日志」「上月 / 这周做过什么」时使用此技能;在带有 ledger/progress.json 的项目里开工之前,也用它查一下当前在办任务。非代码工作区(个人事务、运维杂事、调研、备忘)同样适用。
4
- description_zh: "任务进度管理:新建 / 认领 / 交接 / 改状态 / 结案 / 查在办 / 查历史"
5
- description_en: "Manage task progress: create, claim, hand over, update, close, query in-progress and closed history"
6
- version: 1.0.0
3
+ description: 本地任务台账,跨会话记录在办任务的归属、进度与起止。当用户要登记一件新的事(新建任务 / 建个坑 / 这活我领了)、记录已有任务的进展(接着做 / 记一条进展 / 这轮先记下)、查看任务状态或历史(现在该做什么 / 这活谁在做 / 上周做过什么)时加载;对某个具体任务要求结案或退回时,只认用户的明确指令;只是改代码、讨论方案时不要用。
4
+ version: 1.2.1
7
5
  allowed-tools: Read,Write,Bash,Grep
8
6
  ---
9
7
 
10
8
  # task-ledger
11
9
 
12
- ## 它管什么
10
+ 台账只管三件事:**谁在做、做到哪、什么时候建 / 结**。状态只在 `progress.json`,叙事只在 worklog,**全部经脚本改**。
13
11
 
14
- | 文件 | 装什么 | 谁写 |
15
- |---|---|---|
16
- | `ledger/progress.json` | 在办任务的状态:任务号 / 标题 / 创建人 / owner / 状态 / 时间 / **分类(`category`)与标签(`tags`)** / 过程文档路径(`worklog` 字段) | **只由脚本写** |
17
- | `ledger/worklog/<年月>/<日>-<时分>-<说明>-<随机码>.md` | 该任务的过程正文,固定五节:背景 / 待办 / 下一步 / 过程 / 结论 | **建档只由脚本按模板做**,之后正文由 agent 直接编辑 |
18
- | `ledger/history/<年月>.md` | 已结案任务,一行一条(8 列:结案时间 / 创建时间 / 工作包 / 分类 / 标签 / **owner** / 摘要 / 过程文档);按**结案时间**分月。存量 `ledger/PROGRESS-HISTORY.md` 只读兼容、永不重写 | **只由脚本写**;查询走 `history` 子命令 |
12
+ ## 三个文件
19
13
 
20
- 这三个文件之外不要另建副本。**不要直接编辑状态表和历史索引 —— 一律走脚本。**
21
- 过程文档的**建档也一律走脚本**(`new`):脚本按模板生成五节骨架、把路径写进状态表;
22
- 你只负责往里填正文,**不要自己拼文件名、也不要另建一个 md 顶替它**。
14
+ | 文件 | 装什么 |
15
+ |---|---|
16
+ | `ledger/progress.json` | 在办任务:标题 / owner / 状态 / 时间(`created` `started` `updated` `finished`)/ 分类标签 / 过程文档路径。**只由脚本写** |
17
+ | `ledger/worklog/…md` | 任务正文,六节:背景 / 待办 / 下一步 / 过程 / 结论 / **执行记录**(末节) |
18
+ | `ledger/history/<年月>.md` | 已结案,一行一条。**只由脚本写** |
23
19
 
24
- **台账 ≠ 产出物**:台账(`ledger/`)只装「什么时候建 / 谁在做 / 做到哪 / 什么时候结案」
25
- 以及**指向成果的链接**;真正的成果(代码、方案、清单、报告)按**所在项目自己的约定**落盘,
26
- **不进 `ledger/`**。两条链不交叉 —— 成果是给人用的,台账是给「未来的自己」查的。
20
+ 前五节你直接编辑;**末节「执行记录」只由脚本追加,一个字都不许手写**。
21
+ 不另建副本、不手改状态表和历史、不自己拼 worklog 文件名(`new` 会建好并给路径)。
22
+ **台账 ≠ 产出物**:成果按所在项目自己的约定落盘,台账只放「建/做/结」和指向成果的链接。
27
23
 
28
24
  ## 什么时候用
29
25
 
30
- ✅ 该用:
31
-
32
- - 「现在该做什么」「我还有什么没做完」「谁在做这个」
33
- - 「新建一个任务」「我要做 X」
34
- - 「这活我领了」「这个我来做」
35
- - 「卡住了」「先放一放」「转给某人来做」
36
- - 「这活做完了」「结案」
37
- - 「某件老活是什么时候做的」
38
- - 「谁改过什么」「拉某人的工作日志」
39
- - 在已有 `ledger/progress.json` 的项目里开工之前,先查一眼在办任务
40
-
41
- ❌ 不该用:
26
+ ❌ 项目没有 `ledger/` 且只是临时一次性的事 → 不要自动 `init`,先问。
27
+ ❌ 只想改某任务正文 → 用 `show` 拿路径与全文直接改,不要另建 md;已结案的用 `history` 找链接。
42
28
 
43
- - 项目没有 `ledger/` 且只是临时一次性的事 → **不要自动 init**,先问用户
44
- - 只想改某个任务的过程正文 → 那是编辑 Markdown:`show` 会把该文档**全文和路径一起**给你,直接在它上面改(文档已由脚本建好,不要另建一个)。**已结案**任务的过程文档别用 `show` 找(它可能已不在状态表)—— 用 `history` 拿到文档链接再读
29
+ ## 意图 → 命令
45
30
 
46
- ## 先判断意图,再选命令
47
-
48
- | 用户说什么 | 意图 | 动作 |
49
- |---|---|---|
50
- | 「现在该做什么」「我还有啥没做完」 | 查自己的在办 | `list --mine` |
51
- | 「谁在做 X」「有哪些在办任务」 | 查全部在办 | `list` |
52
- | 「有没有卡住的」 | 按状态查 | `list --status blocked` |
53
- | 「某件老活是什么时候做的」 | 查已结案 | `history`(只读,可直接跑) |
54
- | 「谁改过什么」「拉某人的工作日志」 | 按人查已结案 | `history --owner "<名字>"`(**精确匹配**) |
55
- | 「这周 / 这个月建了啥活」 | 按创建时间查已结案 | `history --since 2026-10-01` |
56
- | 「新建一个任务」「我要做 X」 | 建任务 | `new` |
57
- | 「这活我领了」「我来做这个」 | 认领 | `claim` |
58
- | 「卡住了」「先放一放」 | 改状态 | `set --status blocked` |
59
- | 「继续做」「解卡了」 | 改状态 | `set --status doing` |
60
- | 「转给某人」「交接给某人」 | 换负责人 | `set --owner "<新负责人>"` |
61
- | 「这活做完了」「验收通过」「结案」 | 结案归档 | `close`(**这句必须由用户说**) |
62
- | 项目里还没有状态表 | 初始化 | `init` |
63
- | 状态表看着不对劲 | 体检 | `validate` |
31
+ | 用户说 | 动作 |
32
+ |---|---|
33
+ | 「现在该做什么」「我还有啥没做完」 | `list --mine` |
34
+ | 「谁在做 X」「有哪些在办」 | `list`;在等认领的加 `--status open` |
35
+ | 「这个任务到哪一步了」「这轮做了什么」 | `show --id`(任务 JSON + worklog 全文) |
36
+ | 「某件老活什么时候做的」「谁做过哪些活」 | `history`(可按 `--owner` / `--since` 筛) |
37
+ | 「哪些任务停太久」 | `list --json`:`updated` 满 3 天=停滞、`started` 满 7 天=阻塞(看板算,别改状态) |
38
+ | 「新建一个任务」 | `new`(`--self` = 建完即认领) |
39
+ | 「这活我领了」 | `claim`(只对 `open` 有效) |
40
+ | 「开始做 / 接着做 X」 | 还是 `open` 就先 `claim`;已是 `doing` 就直接改正文 + `log` |
41
+ | 「继续做」「推进一下」 | 改「下一步 / 过程」,再 `log --note "本轮做了 X,下一步 Y"` |
42
+ | 「记一下这轮」「这段做完了」 | `log --id <任务号> --note "…"`(agent 可主动做) |
43
+ | 「卡住了」「先放一放」 | 写清卡在哪(不改状态),再 `log` |
44
+ | 「退回待认领」「让别人接」 | `set --status open`(记 `release`),回待认领 |
45
+ | 「转给某人」 | **skill 不换人**:你先释放,对方自己 `claim`;不许替用户写别人的名字 |
46
+ | 「彻底不做了」「取消这个任务」 | `close --summary "放弃:原因"`(需用户明说) |
47
+ | 「做完了」「验收通过」「结案」 | `close`(需用户明说,见红线) |
48
+ | 「把某任务过程补一下」 | `show --id` → 改前五节 → `log` |
49
+ | 项目还没有台账 / 状态表可疑 | `init`(先问)/ `validate` |
50
+ | 「标签太乱」「两个标签一个意思」 | `tags --all` 看频次 → `tags --rename 旧 新` |
64
51
 
65
52
  ## 命令怎么调
66
53
 
67
54
  ```bash
68
- python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <当前工作区> [参数]
55
+ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <当前工作区>
69
56
  ```
70
57
 
71
- > 下表所有命令都省略了 `--repo`,**实际调用时必须带上当前工作区**。
72
-
73
- - **脚本位置**:用「本 SKILL.md 所在目录」拼出 `scripts/progress.py` 的路径。**不要依赖任何框架注入的变量**(`{baseDir}` / `$SKILL_DIR` 这类各家 harness 各不相同、未必存在);若执行环境就在本 skill 目录内,可直接用 `scripts/progress.py`
74
- - **`--repo` 传当前工作区(项目根)**:脚本只知道自己在哪,不可能知道你在哪个项目里干活,这一步必须由调用方给出
75
- - 参数细节用 `<子命令> --help` 查
58
+ 子命令:`init` `new` `claim` `set` `log` `close` `list` `history` `show` `tags` `validate`。
59
+ 参数细节一律看 `<子命令> --help`(不要凭记忆编参数)。
60
+ 写类命令返回 worklog 路径 + 全文;`list` / `history` 只返回摘要,不吐正文。
76
61
 
77
- | 子命令 | 作用 | 关键参数 |
78
- |---|---|---|
79
- | `init` | 建 `ledger/progress.json` | — |
80
- | `new` | 新建任务,并自动建过程文档 | `--title`(必填)、`--id`(可选)、`--self`、`--created`(可选,补录创建时间)、`--category` / `--tags`(可选,分类 / 标签,见下节口径) |
81
- | `claim` | 认领一个待认领任务 | `--id`(必填) |
82
- | `set` | 改状态 / 换负责人 | `--id`(必填)、`--status`、`--owner` |
83
- | `close` | 结案:移出状态 + 追加历史 + 封卷过程文档 | `--id`、`--summary`(都必填)、`--finished`(可选,结案时间)、`--category` / `--tags`(可选,结案时覆盖修正一次) |
84
- | `list` | 查询在办任务 | `--mine`、`--owner`、`--status`、`--since`、`--until`、`--category`(精确)、`--tag`(包含)、`--json` |
85
- | `history` | 查已结案任务(只读) | `--owner`、`--mine`、`--since`、`--until`、`--limit`、`--category`(精确)、`--tag`(包含)、`--json`;另:`--count-only`(只数行数、封顶 1 万输出 `{count, capped}`,显示级统计)、`--months N`(只读最近 N 个月文件,不含存量平铺文件) |
86
- | `show` | 单任务详情 | `--id`(必填) |
87
- | `tags` | 分类/标签巡检与归并 | `--all`(全量频次表)、`--rename 旧 新`(归并在办任务的同值) |
88
- | `validate` | 校验状态表与过程文档是否一致 | — |
89
-
90
- ### 命令返回什么
91
-
92
- | 命令 | 返回 |
93
- |---|---|
94
- | `new` / `claim` / `set` / `show` | 状态行 + **该任务过程文档的路径与全文** —— 拿到就能直接编辑,不用再去找文件、拼路径(`history` 是例外:只给链接不给全文) |
95
- | `close` | 结案结果与摘要 |
96
- | `list` | 在办任务列表(**不含**文档正文,避免撑爆上下文) |
97
- | `history` | 已结案紧凑列表(结案时间 / 创建时间 / owner / 工作包 / 过程文档链接,行尾附所在历史文件),**默认不含摘要**;要摘要走 `--json` |
62
+ ## 铁律
98
63
 
99
- ### 几个必须知道的点
64
+ - **状态只有 `open` / `doing`**;`done` 只能由 `close` 产生。**没有 `blocked` 状态**:阻塞 = `started` 满 7 天、停滞 = `updated` 满 3 天,都由看板按时间算,`progress.json` 里不存、不手改;卡在哪写进正文。
65
+ - **`log` 就是刷新 `updated` 的动作**:该记不记,任务会被看板标成停滞。
66
+ - **owner = 当前 `git config user.name`**,不能由外部传入;读不到就报错(先让用户配 git)。**只能动自己名下**的任务(`open` 的先 `claim`),不是你的直接拒绝 —— 不要绕过脚本改 JSON。
67
+ - **换人只能「释放 → 认领」**:skill 不提供交接,随便写的名字都是假的。
68
+ - **「执行记录」禁手改**:只由脚本按 `new / claim / set / release / log / close` 追加,只增不改;手写=伪造时间线,且不刷新 `updated`。
69
+ - **升级前建的老 worklog 没有这一节**:不用你补,脚本第一次记时自动在文末补一节;文档丢失时 `log` 报错(`updated` 已刷新),用 `show` 查路径,**不要手写**。
70
+ - **旧数据里的 `blocked`**:读侧当进行中;`list --status blocked` 可捞出,用 `set --status doing` 归位。
100
71
 
101
- - **状态取值**:`open`(已建待认领)/ `doing` / `blocked` / `done`。**`done` 只能由 `close` 产生**,不要用 `set --status done`。
102
- - **`new --self`** = 建完即认领(直接 `doing`);**不带 `--self`** = 空开,等别人 `claim`。
103
- - **任务号自动生成**:创建时间(`年月日-时分`)+ 标题里的字母、数字、汉字 + 4 位随机码(如 `20261015-1430-登录接入-sso-7f3a`)。想指定就传 `--id`。
104
- - **owner 取 `git config user.name`**:认领人**只能是当前 git 身份,不能由外部传入**;读不到就直接报错退出(提示「需要用户名才能执行」),不做任何兜底 —— 先让用户配好 git 身份。(`history` 只有 `--mine` 会取这个身份;`--owner <别人>` 是只读查询,不校验身份)
105
- - **只能动自己名下的任务**:`set` 与 `close` 同样先取身份,再校验 `owner` 是不是你。**不是你的直接拒绝**;任务还是 `open`(没人认领)时也拒绝,提示先 `claim`。别人要动你的任务,必须你先交接(`set --owner`)。**`history` 是只读查询,不在此列** —— 查别人的已结案不需要对方交接。
72
+ 细节见 `references/state-model.md`。
106
73
 
107
- ## 时间
74
+ ## 过程文档
108
75
 
109
- - **精度**:任务号、worklog 目录与文件名只到**分**(`年月日-时分`);其余所有记录时间都到**秒**(`YYYY-MM-DD HH:MM:SS`)。
110
- - **谁来给**:默认不传 —— 脚本取当前时刻(`new` 创建、`claim` / `set` 开始、`close` 结案)。**只在「要记的时间不是现在」时才传**:
111
- - `close --finished`:用户明说这活是什么时候完成的(不传=当前时刻)。
112
- - `new --created`:补录一个过去的创建时间(不传=当前时刻)。
113
- - **怎么写**:格式随意、只要对 —— `2026-10-06`、`2026-10-06 14:30`、`2026-10-06 14:30:05` 都行,脚本补全到秒(没传秒补 `:00`,没传分秒补 `00:00`);传了但不对(如 `2026-13-40`)会被拒。
114
- - **结案时间必须存在**:`close` 一定写(不传=当前)。
115
- - **按时间筛**:`list` / `history` 都有 `--since`(起)/ `--until`(止),都按**创建时间**,可只给其一;只给日期时,起=当天 `00:00:00`、止=当天 `23:59:59`(含整天)。`history` 输出里**第一列是结案时间、第二列才是创建时间**,排序也按创建时间。
76
+ 1. **建档**:`new` 按模板建六节骨架并把路径写进状态表。
77
+ 2. **推进**:你直接编辑前五节,标题不许改名;改完立刻 `log --note "这轮做了什么"`。
78
+ 3. **交付(待验收)**:任务仍留 `doing`,「待办」写「待用户验收」,「下一步」写清验收该看什么;这轮**照样要走收工三步**。结论只写可验证事实,评价等用户验收。
79
+ 4. **封卷**:`close` 打封卷标记,此后不再改;文档靠 `history` 给的回链找回。
116
80
 
117
- ## 过程文档的生命周期
81
+ 细节见 `references/worklog.md`。
118
82
 
119
- 1. **建档** —— 执行 `new` 时,脚本**必须**按 `assets/worklog.md` 模板创建 `ledger/worklog/<年月>/<日>-<时分>-<说明>-<随机码>.md`(`<年月>` 取任务创建月份,如 `202610`;年月已经在目录名里,文件名不再重复)。路径同时写进状态表的 `worklog` 字段,查的时候不必靠推算。
120
- 2. **推进** —— 这个 md 由 **agent 直接编辑**(它是正文,不是状态数据,不走脚本)。五节(背景 / 待办 / 下一步 / 过程 / 结论)**标题结构不能动**。**各写什么、写成什么样算对,模板注释里给了正例**,照它填即可。
121
- 3. **交付(待验收)** —— 活干完了但**用户还没验收**时,任务仍留在 `doing`:worklog 的「待办」写「待用户验收」,「下一步」写清验收该看什么(哪条命令、哪个文件)。**这一段不产出状态变化,只写正文。**
122
- 4. **封卷** —— 执行 `close` 时,脚本自动打封卷标记并把头部状态改为完成,此后不再修改。历史行按**结案时间**落 `ledger/history/<年月>.md`(存量 `ledger/PROGRESS-HISTORY.md` 只读、不再写入);封卷后要找回这篇文档:`history` 会给出它的链接与所在历史文件(只读、无需确认)。
83
+ ## 每次收工必做(「收工」= 这一轮对用户有可见产出)
123
84
 
124
- ## 分类与标签(Agent 自行总结,不设枚举)
85
+ 1. 改正文:这轮变化的事实都落笔,「下一步」写具体动作,不写「继续推进」。
86
+ 2. 跑 `log`:一轮至少一条,正文没变也要记(同时重写「下一步」)。
87
+ 3. 回读末行(`tail -n 3 <路径>` 或 `show --id`):时间 = 现在、说明 = 这轮的事。
125
88
 
126
- **为什么不设枚举**:本 skill 跨所有工作区(开发 / 运维 / 个人事务 / 调研备忘),任何固定分类都覆盖不全、颗粒度难定。所以分类与标签都由 Agent 在 `new` 建档时按本节口径**自行总结**,脚本只硬校验形态(≤4 字 / 纯英文 ≤3 词 / ≤5 个),不校验语义。
89
+ 交付前必须走完这三步;不走,这轮等于没发生,看板还会标停滞。
127
90
 
128
- | 层 | 答什么 | 约束 | 用途 |
129
- |---|---|---|---|
130
- | `--category`(单值) | 这事**是什么类型**(缺陷修复 / 调研 / 建设 / 运维 / 事务 / 其他) | 只有一个;吃不准就写「其他」 | 人眼扫视 + 看板统计(一条一值,`count by` 干净) |
131
- | `--tags`(1~5 个) | 这事**涉及什么**(对象 / 领域 / 模块,如 `镜像拉取`、`Hindsight`) | 至多 5 个;宁缺毋滥,**不凑数** | 下钻查询(包含匹配,容忍变体) |
91
+ **什么算一个节点**:明天你自己想不起来、或换个人接手会看不出的事,就记一条;一次状态/结论变化对应一条,不对应一次工具调用。
92
+ `--note` 一句一条、约 60 字、含命令 / `文件:行号` / 符号名 / 结论之一。
93
+ 不记:读文件、跑 `list`、改错别字、依赖锁文件变更。
132
94
 
133
- 三条边界(用户确认,勿破):
95
+ 细节与正反例见 `references/log-note.md`。
134
96
 
135
- 1. **口径错开**:tags 不许写 category 的同义词 —— category=缺陷修复 时,tags 别再写「bug修复」
136
- 2. **数量软约束**:3~5 个是目标不是硬指标 —— 设下限会逼出凑数标签,凑的正是噪声;脚本只卡上限
137
- 3. **统计与查询分工**:统计只认 category(精确匹配,同值才能比较);tags 只做查询(包含匹配,「bug」命中「bug修复」正是要的效果)
97
+ ## 分类与标签
138
98
 
139
- 写法口径(归一化,跨工作区才稳):
99
+ `new` 时自行总结:`--category` 答「是什么类型」(单值,≤4 字,吃不准写「其他」);`--tags` 答「涉及什么」(0~5 个,写不出就留空)。
100
+ 口径错开:tags 不许写 category 的同义词。统计只认 category,查询用 tags(包含匹配)。
140
101
 
141
- - 名词性短语,不带时态、不用动词收尾:写「缺陷修复」,不写「修复了」「修 bug」
142
- - 中文优先,不中英混排;纯英文 ≤3 个单词(`fix a bug` 可以,`fix a login bug` 超了)
143
- - 标签之间用**逗号**分隔 —— 空格是英文多词标签自身的,不当分隔符(`--tags "fix a bug,docker"`)
144
- - 正例:`--category 缺陷修复 --tags "镜像拉取,超时重试"`;反例:`--category 已完成登录修复`、`--tags 修 bug`
145
-
146
- 时机与修正:`new` 建档时一并总结(标题 + 背景语义最全);`close` 可用 `--category` / `--tags` 覆盖修正一次;日常漂移用 `tags --rename 旧 新` 归并(只改在办任务,已封卷文档不动)。看板侧先跑 `tags --all` 看全量频次与低频变体,再决定查什么、归并什么。已结案记录的分类/标签落在 history 行的「分类/标签」列(8 列结构,close 时从 task 终值写入)。
102
+ 细节见 `references/taxonomy.md`。
147
103
 
148
104
  ## 写操作红线
149
105
 
150
- `init` / `new` / `claim` / `set` / `close` 都有副作用,但**分两档**:
151
-
152
- **一、动手类(`init` / `new` / `claim` / `set`)**:执行前先把完整参数复述给用户并取得确认(任务号、标题、负责人、目标状态)。不得先执行再补报告,也不得一次执行多条用户没逐条确认的写操作。
153
-
154
- **二、`close` 是独立授权,不是做任务流程的最后一步**:
106
+ | 命令 | 档位 | 要求 |
107
+ |---|---|---|
108
+ | `init` / `new` / `claim` / `set` | 动手类 | 执行前把完整参数复述给用户并取得确认 |
109
+ | `log` | 免确认 | 本任务的推进 / 卡点 / 交付记录直接跑,跑完一句话交代「已记执行记录:…」;要替用户下结论、或替别人记,先问 |
110
+ | `close` | 独立授权 | 只认用户明确的验收 / 结案 / 放弃指令;**agent 自判「干完了」不算** —— 那是交付,停在 `doing` 等用户回话 |
111
+ | `tags --rename` | 批量写 | 先 `tags --all`,把影响条数与任务号报给用户,确认后再改 |
112
+ | `list` / `show` / `history` / `validate` / `tags --all` | 只读 | 直接跑 |
155
113
 
156
- - 只能由用户**明确的验收 / 结案指令**触发(「验收通过」「这活结了吧」)。
157
- - **agent 自己判断「我干完了」不算触发条件** —— 那是交付,不是结案。交付后停在 `doing`,把做了什么、`--summary` 打算怎么写交给用户,**等用户回话**(这段时间 worklog 怎么记见下节)。
158
- - 用户没点头就不许先斩后奏,也不许替用户下验收结论。**宁可任务停在 `doing`,也不要悄悄进历史。**
114
+ ## 出错即停
159
115
 
160
- `list` / `show` / `validate` / `history` 是只读,可以直接跑。
116
+ 非 0 退出就停手、原样报告,**不要绕过脚本手改 `progress.json` 或历史层**。
117
+ 并发冲突(文件被改动)重跑一次即可,不要用 `git pull` / `rebase` 去「解决」。
161
118
 
162
- ## 失败处理
119
+ 细节见 `references/troubleshooting.md`。
163
120
 
164
- 脚本失败时以非 0 退出码 + stderr 说明原因:
121
+ ## references(按需读,别全读)
165
122
 
166
- | 退出码 | 含义 |
123
+ | 何时读 | 文件 |
167
124
  |---|---|
168
- | 0 | 成功 |
169
- | 1 | 业务错误(参数值 / 状态 / 权限不对,可纠正) |
170
- | 2 | 命令行参数错误 |
171
- | 3 | 未预期的内部失败(应上报,不要重试) |
172
- | 130 | 被中断 |
173
-
174
- **报错就停下并原样报告,不要绕过脚本直接改 `ledger/progress.json` 或历史层(`ledger/history/<年月>.md`)。**
175
-
176
- 并发冲突(文件在操作期间被改动)会报错并中止 —— 重新执行一次即可,**不要用 `git pull` / `rebase` 去「解决」**。
125
+ | 要传 `--created` / `--finished` 补录时间、看到旧 `blocked`、答「停太久」要解释口径 | `references/state-model.md` |
126
+ | 填正文前、补老任务文档、`log` 报错找不到文档 | `references/worklog.md` |
127
+ | 拿不准一条记录够不够格、`--note` 怎么写 | `references/log-note.md` |
128
+ | 写分类/标签、用户说标签太乱要归并 | `references/taxonomy.md` |
129
+ | 任何非 0 退出、并发冲突、`validate` 报错 | `references/troubleshooting.md` |
package/assets/worklog.md CHANGED
@@ -2,30 +2,29 @@
2
2
 
3
3
  > **创建**:<created>(@<creator>)
4
4
 
5
- <!-- 「> **分类**:… | **标签**:…」行由脚本按 new --category / --tags 自动插在创建行后;
6
- 两者皆空则不写此行。close 时可用 --category / --tags 覆盖修正。 -->
5
+ <!-- 「> **分类**:… | **标签**:…」行由脚本按 new --category / --tags 插入;两者皆空则不写。 -->
7
6
 
8
7
  ## 背景
9
8
 
10
- <!-- 为什么要做这件事。写触发它的原话、现象或链接,不要写「优化 XX」这类过三个月看不懂的。
11
- 例:用户说「导出的 Excel 中文列名乱码」(附原话 / 截图 / 报错) -->
9
+ <!-- 为什么做:触发它的原话 / 现象 / 链接 -->
12
10
 
13
11
  ## 待办
14
12
 
15
- <!-- 还没做的、卡在哪、依赖谁。做到一半的、没验证的、等别人给东西的,都算。
16
- 例:等宿主 1.2 才能复测流式接口;错误分支尚未测 -->
13
+ <!-- 还没做的、没验证的、等别人给东西的 -->
17
14
 
18
15
  ## 下一步
19
16
 
20
- <!-- 写到具体做哪一步,不是「继续推进」。
21
- 例:改 src/export.ts:88 的编码参数,然后跑一次冒烟 -->
17
+ <!-- 下一个具体动作(文件:行号 级别),不要写「继续推进」 -->
22
18
 
23
19
  ## 过程
24
20
 
25
- <!-- 踩坑、定位、证据。带 revision 和 文件:行号 或符号名(行号会漂移)。
26
- 例:rc4f2a1 的 src/host.ts:112 返回 null,是因为没等 ready 事件就发了请求 -->
21
+ <!-- 踩坑与证据:带 revision + 文件:行号 或符号名 -->
27
22
 
28
23
  ## 结论
29
24
 
30
- <!-- 收尾时填:做成了什么、留了什么坑。
31
- 例:已修复,但只覆盖 UTF-8;GBK 另开任务 -->
25
+ <!-- 做成了什么、留了什么坑 -->
26
+
27
+ ## 执行记录
28
+
29
+ <!-- 本节由脚本维护:new / claim / set / release / log / close 自动追加,请勿手改。
30
+ 补记用 progress.py log --id <任务号> --note "一句话"。 -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-ledger",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Task-ledger skill for coding agents: per-workspace progress tracking (ledger/progress.json, 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",
@@ -18,6 +18,7 @@
18
18
  "SKILL.md",
19
19
  "README.md",
20
20
  "scripts/",
21
- "assets/"
21
+ "assets/",
22
+ "references/"
22
23
  ]
23
24
  }
@@ -0,0 +1,47 @@
1
+ # 执行记录:什么算一个节点
2
+
3
+ > TL;DR:一件事「明天你想不起来、换个人接手看不出」就记一条;一次状态/结论变化对应一条,不对应一次工具调用。`--note` 一句 60 字内,必须含具体对象。
4
+
5
+ ## 判据
6
+
7
+ **明天你自己想不起来、或换个人接手会看不出** —— 就是一个节点,记一条。
8
+ 一条 `log` 对应**一次状态变化或一次结论变化**,不对应一次工具调用。
9
+
10
+ ## 该记
11
+
12
+ | 触发 | `--note` 写成什么 |
13
+ |---|---|
14
+ | 用户提了新要求 / 改了目标 | 新要求是什么,覆盖了哪条旧做法 |
15
+ | 方案变更 / 方案被排除 | 新方案(或排除了哪个)+ 为什么 |
16
+ | 改完一批文件 | 动了哪几个模块、验证没有 |
17
+ | 跑完验证(**通过或失败都算**) | 跑了什么、结果、下一步是否因此变 |
18
+ | 被卡住 / 停下 / 等外部 | 卡在哪一环、缺什么、等谁 |
19
+ | 拿到关键外部事实(版本 / 接口形状 / 已核实结论) | 结论 + 来源标识,免得下次重新查 |
20
+ | 交付(待验收) | 交付了什么、验收该看哪 |
21
+ | 释放 / 重新认领 | 脚本已自动记,别重复记 |
22
+
23
+ ## 不该记
24
+
25
+ 读文件、查命令、跑 `list`、想了一下;纯格式化 / 改错别字 / 依赖锁文件自动变更 —— 这些留在会话或 git 里。
26
+
27
+ ## 写法
28
+
29
+ - 一句一条,`--note` 会被压成单行;**第一句说明白做了什么**,约 60 字以内,细节写正文对应小节。
30
+ - 一轮做了明显不相关的两件事 → 记两条,别拼成一条。
31
+ - 自检:一条记录至少能回答「动了什么」—— 含命令 / `文件:行号` / 符号名 / 结论之一。
32
+
33
+ 正例:
34
+
35
+ ```
36
+ --note "把 home.service 的停滞口径改成 updated 满 3 天(home.service.ts:41-52)"
37
+ --note "验证通过:verify 25 项;下一步补 UI 的 overdue 标记"
38
+ --note "卡在远端鉴权,缺 deploy key,等用户给"
39
+ ```
40
+
41
+ 反例:
42
+
43
+ ```
44
+ --note "继续推进" # 零信息量
45
+ --note "我改了代码,应该没问题了" # 无具体对象 + 替用户下结论
46
+ --note "改了 a.ts,又跑了测试,还回了消息" # 三件事挤一条
47
+ ```
@@ -0,0 +1,43 @@
1
+ # 状态与时间模型
2
+
3
+ > TL;DR:状态只有 `open` / `doing`;`done` 由 `close` 产生。阻塞 = `started` 满 7 天、停滞 = `updated` 满 3 天,看板算、不存字段。`updated` 由 `new/claim/set/log` 刷新。owner 只取当前 git 身份。
4
+
5
+ ## 状态机
6
+
7
+ ```
8
+ open ──claim──> doing ──close──> ledger/history(移出状态表)
9
+ doing ──set --status open──> open # 释放认领,执行记录类型 release
10
+ ```
11
+
12
+ - `open`:已建、等认领。`doing`:已认领、在做。`done` 只由 `close` 产生。
13
+ - **没有 `blocked` 状态**(2026-10-19 取消)。旧数据里残留的 `blocked` 读侧当进行中:`list --status blocked` 可捞出,用 `set --status doing` 归位;新写入一律拒绝。
14
+
15
+ ## 两个标记(看板计算,不是状态)
16
+
17
+ | 标记 | 依据 | 阈值 |
18
+ |---|---|---|
19
+ | 阻塞 | `started`(接任务)至今,中途更新不影响 | 满 7 天 |
20
+ | 停滞 | `updated`(最后推进)至今 | 满 3 天 |
21
+
22
+ - 真源:`packages/plugin/src/host/services/home.service.ts` 的 `OVERDUE_DAYS = 7` / `STALE_DAYS = 3`;看板随响应下发 `staleDays`,界面文案不硬编码。
23
+ - 脚本不算这两个值,`progress.json` 里也不存;agent 要回答「哪些停太久」时用 `list --json` 的 `updated` / `started` 与当前时间比。
24
+ - 老数据没有 `updated` 时,读侧回退到 `started`,再到 `created`。
25
+
26
+ ## 时间
27
+
28
+ - **精度**:任务号、worklog 目录与文件名到分(`年月日-时分`);其余记录到秒(`YYYY-MM-DD HH:MM:SS`)。
29
+ - **默认不传**:`new` 创建、`claim` / `set` 推进、`log` 刷新 `updated`、`close` 结案,脚本都取当前时刻。
30
+ - **只在补录过去时间时传**:`new --created`、`close --finished`。格式 `YYYY-MM-DD[ HH:MM[:SS]]`,脚本补全到秒;非法值拒绝。
31
+ **`new --created` 补录时 `updated` 会一并等于该时间**,之后记得补一条 `log`,否则看板算它停滞。
32
+ - **筛选语义**:`list` / `history` 的 `--since` / `--until` 按**创建时间**;只给日期时,起 = 当天 `00:00:00`、止 = 当天 `23:59:59`(含整天)。
33
+ - **history 列序**:第一列结案时间、第二列创建时间;排序按创建时间。
34
+
35
+ ## 任务号
36
+
37
+ `年月日-时分` + 标题里的字母/数字/汉字 + 4 位随机码(如 `20261015-1430-登录接入-sso-7f3a`)。要指定就传 `--id`(小写字母、数字、汉字、连字符)。
38
+
39
+ ## 身份
40
+
41
+ - owner 恒取 `git config user.name`;读不到直接报错,**不做兜底**(先让用户配好 git)。
42
+ - 取身份的命令:`new`(记 creator)、`claim`、`set`、`close`、`log`、`list --mine`、`history --mine`。`history --owner <别人>` 是只读查询,不校验身份。
43
+ - `set` / `close` / `log` 都校验「owner 是不是你」;不是你的、或任务还没人认领,直接拒绝。
@@ -0,0 +1,26 @@
1
+ # 分类与标签
2
+
3
+ > TL;DR:`category` 答「是什么类型」(单值,≤4 字,吃不准写「其他」);`tags` 答「涉及什么」(0~5 个,写不出就留空)。两者口径错开;统计只认 category,查询用 tags。
4
+
5
+ 为什么不设枚举:本 skill 跨所有工作区(开发 / 运维 / 个人事务 / 调研备忘),固定分类覆盖不全。分类与标签由 agent 在 `new` 时按本节口径自行总结;脚本只校验形态(≤4 字 / 纯英文 ≤3 词 / ≤5 个),不校验语义。
6
+
7
+ ## 两条边界
8
+
9
+ 1. **口径错开**:tags 不许写 category 的同义词 —— `category=缺陷修复` 时,tags 别再写「bug修复」。
10
+ 2. **统计与查询分工**:统计只认 category(精确匹配,同值才能比较);tags 只做查询(包含匹配,「bug」命中「bug修复」正是要的效果)。
11
+ 3. **数量不设下限**:3~5 个是目标不是硬指标;设下限会逼出凑数标签。
12
+
13
+ ## 写法口径
14
+
15
+ - 名词性短语,不带时态、不用动词收尾:写「缺陷修复」,不写「修复了」「修 bug」。
16
+ - 中文优先,不中英混排;纯英文 ≤3 个单词(`fix a bug` 可以,`fix a login bug` 超了)。
17
+ - 标签之间用**逗号**分隔;空格属于英文多词标签自身(`--tags "fix a bug,docker"`)。
18
+
19
+ 正例:`--category 缺陷修复 --tags "镜像拉取,超时重试"`
20
+ 反例:`--category 已完成登录修复`、`--tags 修 bug`
21
+
22
+ ## 时机与修正
23
+
24
+ - `new` 建档时一并总结(标题 + 背景语义最全)。
25
+ - `close` 可用 `--category` / `--tags` 覆盖修正一次;终值写进 history 行的「分类 / 标签」列。
26
+ - 日常漂移:先 `tags --all` 看全量频次与低频变体,再 `tags --rename 旧 新` 归并(只改在办任务,已封卷文档不动;批量写,见写操作红线)。
@@ -0,0 +1,26 @@
1
+ # 排错
2
+
3
+ > TL;DR:非 0 退出就停手、原样报告,别绕过脚本改 JSON。并发冲突重跑一次,不要用 `git pull` / `rebase` 去「解决」。
4
+
5
+ ## 退出码
6
+
7
+ | 码 | 含义 | 处置 |
8
+ |---|---|---|
9
+ | 0 | 成功 | — |
10
+ | 1 | 业务错误(参数值 / 状态 / 权限不对) | 按提示纠正后重试;改不了的报告用户 |
11
+ | 2 | 命令行参数错误 | 看 `<子命令> --help` 重新拼参数 |
12
+ | 3 | 未预期的内部失败 | 上报,不要重试 |
13
+ | 130 | 被中断 | 重跑一次 |
14
+
15
+ ## 常见错误
16
+
17
+ - **「需要用户名才能执行」**:`git config user.name` 没配。让用户配好,不要自拟名字。
18
+ - **「不是你的任务 / 先 claim」**:任务 owner 不是你或是 `open`。让别人接手只能走「你释放 → 对方 `claim`」;不要用别的名字绕过。
19
+ - **「状态不一致」**:对已在 `doing` 的任务又跑了 `claim`。直接改正文 + `log` 即可。
20
+ - **`log` 报「没写进过程文档」**:worklog 路径丢失 / 不可读。用 `show --id` 查路径;`progress.json` 的 `updated` 已刷新,不要手写「执行记录」。
21
+ - **「文件在操作期间被改动」**(并发指纹冲突):重新执行一次即可。
22
+ - **`validate` 报问题**:按行号读原文,先核对 `progress.json` 与 worklog 是否一致;能通过脚本修的走 `set` / `log`,**不要手改 JSON**。
23
+
24
+ ## 硬约束
25
+
26
+ 不要直接编辑 `ledger/progress.json`、`ledger/history/*.md`;不要另建 worklog;不要手写「执行记录」节。
@@ -0,0 +1,37 @@
1
+ # 过程文档(worklog)
2
+
3
+ > TL;DR:六节固定;前五节 agent 改,末节「执行记录」只由脚本写。改完正文必须 `log`,交付也要 `log`。老文档缺末节时脚本自动补。
4
+
5
+ ## 六节结构
6
+
7
+ | 节 | 写什么 |
8
+ |---|---|
9
+ | 背景 | 为什么做:触发它的原话 / 现象 / 链接 |
10
+ | 待办 | 还没做的、没验证的、等别人给东西的 |
11
+ | 下一步 | 下一个**具体动作**(文件:行号 级别),不要写「继续推进」 |
12
+ | 过程 | 踩坑与证据:带 revision + `文件:行号` 或符号名 |
13
+ | 结论 | 做成了什么、留了什么坑;只写可验证事实,评价留给用户验收 |
14
+ | 执行记录 | 脚本维护,**禁手改** |
15
+
16
+ 标题结构不能改名。建档只由 `new` 做(按 `assets/worklog.md` 模板生成骨架并写入状态表 `worklog` 字段)。
17
+
18
+ ## 执行记录
19
+
20
+ - 一行一事件:`- 2026-10-19 12:08:33 · log · @GuoJia-Agent · 说明`。
21
+ - 类型:`new`(建)/ `claim`(认领)/ `set`(改状态、owner 变更)/ `release`(`set --status open` 释放)/ `log`(手记)/ `close`(结案)。
22
+ - **只追加、不改旧行**:`@谁` 记的是这次动手的人,不是任务现 owner —— 释放/重新认领后旧记录仍留在原负责人名下。
23
+ - 老 worklog 没有这一节:脚本第一次记时在文末自动补一节(正文不迁移),agent 不用也不许手动加。
24
+ - 过程文档丢失:`log` 报错退出(`updated` 已刷新),用 `show --id` 查路径,**不要手写这一节**。
25
+
26
+ ## 编辑既有任务
27
+
28
+ 1. `show --id` 拿任务 JSON + worklog 全文与路径;
29
+ 2. 直接改前五节;
30
+ 3. 立刻 `log --id <任务号> --note "补了…/改了…"`。
31
+
32
+ 已结案任务:worklog 已封卷、不在状态表,用 `history` 给出的回链找回,只读不改。
33
+
34
+ ## 交付与封卷
35
+
36
+ - **交付(待验收)**:任务仍留 `doing`;「待办」写「待用户验收」,「下一步」写清验收看什么(哪条命令 / 哪个文件);按收工三步记一条 `log --note "已交付待验收:…"`。
37
+ - **封卷**:`close` 打封卷标记,此后不再修改;历史行按结案时间写入 `ledger/history/<年月>.md`(存量 `ledger/PROGRESS-HISTORY.md` 只读兼容,不再写入)。
@@ -37,7 +37,10 @@ try:
37
37
  except ImportError:
38
38
  msvcrt = None
39
39
 
40
- STATUSES = ("open", "doing", "blocked", "done")
40
+ STATUSES = ("open", "doing")
41
+ # 旧数据只读兼容:早期版本有 blocked 状态(手动标「卡住」)。现在阻塞按时间判定
42
+ # (接任务满 7 天),不再允许新写入;list / validate / 看板仍能吃老文件。
43
+ LEGACY_STATUSES = ("blocked",)
41
44
  PROGRESS_REL = Path("ledger/progress.json")
42
45
  # 存量:改造前的单文件历史层,只读兼容、永不重写
43
46
  HISTORY_REL = Path("ledger/PROGRESS-HISTORY.md")
@@ -52,7 +55,7 @@ HISTORY_TITLE = "# 进度历史(已结案)"
52
55
  # 但「谁建的」不变)。看板「新增(我的)= 我创建的」要它;旧 8 列无此列,
53
56
  # upgrade_history / cmd_history 回链 worklog 头部「创建」行补(与 created 同一来源,不丢)。
54
57
  HISTORY_HEADER = "| 结案时间 | 创建时间 | 工作包 | 分类 | 标签 | owner | creator | 完成了什么 | 过程文档 |"
55
- HISTORY_SEP = "|---|---|---|---|---|---|---|---|"
58
+ HISTORY_SEP = "|---|---|---|---|---|---|---|---|---|"
56
59
  FROZEN_MARK = "✅ 完成封卷"
57
60
  ID_RE = re.compile(r"[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*\Z")
58
61
  # 读侧兼容:秒可有可无(旧数据只到分)。写侧一律到秒,见 normalize_time。
@@ -65,6 +68,14 @@ LABEL_MAX_WORDS_EN = 3
65
68
  TAGS_MAX_COUNT = 5
66
69
  CJK_RE = re.compile(r"[\u4e00-\u9fff]")
67
70
 
71
+ # 执行记录:worklog 末尾由脚本维护的一节。老文档没有这一节就不动(不迁移);
72
+ # 有这一节时脚本按节点自动追加,agent 改完正文跑 log 补一条。
73
+ EXEC_HEAD = "## 执行记录"
74
+ EXEC_HINT = (
75
+ "<!-- 本节由脚本维护:new / claim / set / close / log 自动追加,请勿手改。\n"
76
+ ' 要补一条就跑 progress.py log --id <任务号> --note "做了什么"。 -->'
77
+ )
78
+
68
79
  SKILL_DIR = Path(__file__).resolve().parent.parent
69
80
  WORKLOG_TEMPLATE = SKILL_DIR / "assets" / "worklog.md"
70
81
 
@@ -81,6 +92,8 @@ FALLBACK_TEMPLATE = """# <id>:<title>
81
92
  ## 过程
82
93
 
83
94
  ## 结论
95
+
96
+ ## 执行记录
84
97
  """
85
98
 
86
99
 
@@ -1134,6 +1147,58 @@ def find_worklog(
1134
1147
  return None
1135
1148
 
1136
1149
 
1150
+ def exec_line(when: str, who: str | None, kind: str, note: str) -> str:
1151
+ """一条执行记录:`- 时间 · 类型 · @某人 · 说明`。
1152
+
1153
+ 类型取脚本子命令名(new / claim / set / log / close),纯 ASCII、可 grep 统计;
1154
+ 记录只追加不改写 —— 移交后旧记录仍记在原来那个人名下。
1155
+ """
1156
+ head = f"- {when} · {kind}"
1157
+ return f"{head} · @{who} · {note}" if who else f"{head} · {note}"
1158
+
1159
+
1160
+ def append_exec_log(text: str, line: str) -> str:
1161
+ """把一条记录追加到「执行记录」节末尾。
1162
+
1163
+ 升级前建的老 worklog 没有这一节:在文末补一节再写(不迁移正文,也不让记录静默丢掉)。
1164
+ """
1165
+ lines = text.splitlines()
1166
+ at = next((i for i, ln in enumerate(lines) if ln.strip() == EXEC_HEAD), None)
1167
+ if at is None:
1168
+ return text.rstrip() + f"\n\n{EXEC_HEAD}\n\n{EXEC_HINT}\n\n{line}\n"
1169
+ end = next(
1170
+ (i for i in range(at + 1, len(lines)) if lines[i].startswith("## ")), len(lines)
1171
+ )
1172
+ insert = end
1173
+ while insert > at + 1 and not lines[insert - 1].strip():
1174
+ insert -= 1
1175
+ lines.insert(insert, line)
1176
+ return "\n".join(lines) + "\n"
1177
+
1178
+
1179
+ def record_exec(
1180
+ repo: Path, tid: str, task: dict[str, Any], who: str | None, kind: str, note: str
1181
+ ) -> bool:
1182
+ """给任务的过程文档补一条执行记录(`who` = 这次动手的人,不是任务现 owner)。
1183
+
1184
+ 返回是否真的落盘;`log` 用它判断要不要报错。调用方必须已经持有 progress_lock ——
1185
+ 与 close 的封卷(读全文→改写→落盘)互斥,否则两条记录并发追加会互相覆盖。
1186
+ """
1187
+ log = find_worklog(repo, tid, task)
1188
+ if log is None:
1189
+ return False
1190
+ try:
1191
+ text = log.read_text(encoding="utf-8")
1192
+ except (OSError, UnicodeDecodeError):
1193
+ return False
1194
+ line = exec_line(now(), who, kind, note)
1195
+ try:
1196
+ write_text_atomic(log, append_exec_log(text, line))
1197
+ except OSError:
1198
+ return False
1199
+ return True
1200
+
1201
+
1137
1202
  def write_worklog(repo: Path, tid: str, task: dict[str, Any]) -> Path:
1138
1203
  log = repo / worklog_dir_for(task) / worklog_name(tid)
1139
1204
  write_text_atomic(log, render_worklog(tid, task))
@@ -1218,6 +1283,7 @@ def cmd_new(args: argparse.Namespace) -> None:
1218
1283
  "status": "doing" if args.self else "open",
1219
1284
  "created": stamp,
1220
1285
  "started": stamp if args.self else None,
1286
+ "updated": stamp,
1221
1287
  "finished": None,
1222
1288
  "category": category,
1223
1289
  "tags": tags,
@@ -1236,6 +1302,7 @@ def cmd_new(args: argparse.Namespace) -> None:
1236
1302
  except Fail:
1237
1303
  pass
1238
1304
  raise Fail(f"创建失败,已从状态表撤销该任务:{exc}")
1305
+ record_exec(repo, tid, task, creator, "new", f"创建任务({task['status']})")
1239
1306
  print(f"已创建任务 {tid}")
1240
1307
  print(f" 标题 {task['title']}")
1241
1308
  print(f" 状态 {task['status']}")
@@ -1271,10 +1338,13 @@ def cmd_claim(args: argparse.Namespace) -> None:
1271
1338
  f"任务 {tid} 当前状态是 {task.get('status')},只有 open 才能认领。"
1272
1339
  )
1273
1340
 
1341
+ stamp = now()
1274
1342
  task["owner"] = owner
1275
1343
  task["status"] = "doing"
1276
- task["started"] = task.get("started") or now()
1344
+ task["started"] = task.get("started") or stamp
1345
+ task["updated"] = stamp
1277
1346
  commit_guarded(path, before, data)
1347
+ record_exec(repo, tid, task, owner, "claim", "认领 → doing")
1278
1348
  print(f"已认领 {tid} → doing,owner = @{owner}")
1279
1349
  print_worklog(repo, tid, task)
1280
1350
 
@@ -1292,12 +1362,17 @@ def cmd_set(args: argparse.Namespace) -> None:
1292
1362
  raise Fail(
1293
1363
  "--status open(退回未认领)不能同时给 --owner;要交给别人就别改回 open。"
1294
1364
  )
1295
- if status is not None and status not in STATUSES:
1296
- raise Fail(f"--status 只能是 {' / '.join(STATUSES)};结案请用 close。")
1365
+ if status == "blocked":
1366
+ raise Fail(
1367
+ "没有 blocked 状态:阻塞由时间自动判定(接任务满 7 天仍未结案),"
1368
+ "状态只有 open / doing;卡在哪里请写进 worklog。"
1369
+ )
1297
1370
  if status == "done":
1298
1371
  raise Fail(
1299
1372
  "不要把状态直接改成 done —— 用 close 结案(它会同时移出状态、追加历史、封卷 worklog)。"
1300
1373
  )
1374
+ if status is not None and status not in STATUSES:
1375
+ raise Fail(f"--status 只能是 {' / '.join(STATUSES)};结案请用 close。")
1301
1376
 
1302
1377
  repo = find_repo(args.repo)
1303
1378
  tid = require_task_id(args.id)
@@ -1328,11 +1403,40 @@ def cmd_set(args: argparse.Namespace) -> None:
1328
1403
  task["started"] = task.get("started") or now()
1329
1404
  changed.append(f"owner {old or '(无)'} → {task['owner']}")
1330
1405
 
1406
+ task["updated"] = now()
1331
1407
  commit_guarded(path, before, data)
1408
+ # 释放认领(退回 open)单列一个类型,便于一眼看出「被释放过」
1409
+ kind = "release" if status == "open" else "set"
1410
+ record_exec(repo, tid, task, me, kind, ",".join(changed))
1332
1411
  print(f"已更新 {tid}:" + ",".join(changed))
1333
1412
  print_worklog(repo, tid, task)
1334
1413
 
1335
1414
 
1415
+ def cmd_log(args: argparse.Namespace) -> None:
1416
+ repo = find_repo(args.repo)
1417
+ tid = require_task_id(args.id)
1418
+ note = " ".join(args.note.split())
1419
+ if not note:
1420
+ raise Fail("--note 不能为空:一句话写清这轮做了什么。")
1421
+ me = git_user()
1422
+ path = repo / PROGRESS_REL
1423
+ with progress_lock(path):
1424
+ before, data = read_guarded(path)
1425
+ task = data["tasks"].get(tid)
1426
+ if task is None:
1427
+ raise Fail(f"没有任务 {tid}(可能未建或已结案;封卷后不再记)。")
1428
+ require_owner(task, tid, me)
1429
+ task["updated"] = now()
1430
+ commit_guarded(path, before, data)
1431
+ written = record_exec(repo, tid, task, me, "log", note)
1432
+ if not written:
1433
+ raise Fail(
1434
+ f"没写进 {tid} 的过程文档(文件不存在或不可读);progress.json 的 updated 已刷新。"
1435
+ "先用 show --id 确认 worklog 路径,不要手写「执行记录」。"
1436
+ )
1437
+ print(f"已记入 {tid} 的执行记录:{note}")
1438
+
1439
+
1336
1440
  def cmd_close(args: argparse.Namespace) -> None:
1337
1441
  repo = find_repo(args.repo)
1338
1442
  tid = require_task_id(args.id)
@@ -1414,29 +1518,33 @@ def cmd_close(args: argparse.Namespace) -> None:
1414
1518
  if close_tags is not None
1415
1519
  else list(task.get("tags") or []),
1416
1520
  )
1417
- new_log = stamp_frozen(log_text, finished, owner)
1418
1521
  else:
1419
- new_log = stamp_frozen(
1420
- render_worklog(
1421
- tid,
1422
- dict(
1423
- task,
1424
- status="done",
1425
- category=(
1426
- close_category
1427
- if close_category is not None
1428
- else task.get("category")
1429
- ),
1430
- tags=(
1431
- close_tags
1432
- if close_tags is not None
1433
- else list(task.get("tags") or [])
1434
- ),
1522
+ log_text = render_worklog(
1523
+ tid,
1524
+ dict(
1525
+ task,
1526
+ status="done",
1527
+ category=(
1528
+ close_category
1529
+ if close_category is not None
1530
+ else task.get("category")
1531
+ ),
1532
+ tags=(
1533
+ close_tags
1534
+ if close_tags is not None
1535
+ else list(task.get("tags") or [])
1435
1536
  ),
1436
1537
  ),
1437
- finished,
1438
- owner,
1439
1538
  )
1539
+ # 结案本身是最后一个节点:封卷前补上这条记录,封卷后不再追加
1540
+ new_log = stamp_frozen(
1541
+ append_exec_log(
1542
+ log_text,
1543
+ exec_line(finished, me, "close", f"结案:{summary}"),
1544
+ ),
1545
+ finished,
1546
+ owner,
1547
+ )
1440
1548
 
1441
1549
  # 落盘前复查:三个文件都没被别人动过
1442
1550
  for p, mark in (
@@ -1539,18 +1647,13 @@ def cmd_list(args: argparse.Namespace) -> None:
1539
1647
  if args.status is not None:
1540
1648
  wanted = [s.strip() for s in args.status.split(",") if s.strip()]
1541
1649
  if not wanted:
1542
- raise Fail("--status 不能为空;逗号分隔,如 doing,blocked")
1543
- bad = [s for s in wanted if s not in STATUSES]
1650
+ raise Fail("--status 不能为空;逗号分隔,如 doing,open")
1651
+ # blocked 只用于捞旧数据(新任务不可能再有这个值)
1652
+ bad = [s for s in wanted if s not in STATUSES and s not in LEGACY_STATUSES]
1544
1653
  if bad:
1545
1654
  raise Fail(
1546
1655
  f"--status 含非法值 {', '.join(bad)};可选 {' / '.join(STATUSES)}"
1547
1656
  )
1548
- if "done" in wanted:
1549
- print(
1550
- "提示:done 的任务已结案并移出状态表,用 history 子命令查"
1551
- "(已结案在 ledger/history/<年月>.md,存量在 ledger/PROGRESS-HISTORY.md)。",
1552
- file=sys.stderr,
1553
- )
1554
1657
 
1555
1658
  since = normalize_time(args.since, field="--since") if args.since else None
1556
1659
  until = (
@@ -1734,7 +1837,7 @@ def cmd_validate(args: argparse.Namespace) -> None:
1734
1837
  if not task.get("title"):
1735
1838
  problems.append(f"{tid}: 缺 title")
1736
1839
  status = task.get("status")
1737
- if status not in STATUSES:
1840
+ if status not in STATUSES and status not in LEGACY_STATUSES:
1738
1841
  problems.append(f"{tid}: status={status!r} 非法")
1739
1842
  if status == "open" and task.get("owner"):
1740
1843
  problems.append(f"{tid}: status=open 却有 owner")
@@ -2006,10 +2109,16 @@ def build_parser() -> argparse.ArgumentParser:
2006
2109
  p = sub.add_parser("set", help="改状态 / 交接(结案请用 close)")
2007
2110
  p.add_argument("--id", required=True)
2008
2111
  p.add_argument("--status", help=f"{' / '.join(STATUSES)}(done 请走 close)")
2009
- p.add_argument("--owner", help="交接给谁")
2112
+ p.add_argument("--owner", help="交接给谁(仅供 UI / 维护者;agent 提示词不暴露)")
2010
2113
  add_repo(p)
2011
2114
  p.set_defaults(func=cmd_set)
2012
2115
 
2116
+ p = sub.add_parser("log", help="往 worklog 的「执行记录」节补一条记录")
2117
+ p.add_argument("--id", required=True)
2118
+ p.add_argument("--note", required=True, help="一句话:这轮做了什么")
2119
+ add_repo(p)
2120
+ p.set_defaults(func=cmd_log)
2121
+
2013
2122
  p = sub.add_parser("close", help="结案:移出状态 + 追加历史 + 封卷 worklog")
2014
2123
  p.add_argument("--id", required=True)
2015
2124
  p.add_argument("--summary", required=True, help="做成了什么,一句话")
@@ -2024,7 +2133,7 @@ def build_parser() -> argparse.ArgumentParser:
2024
2133
  p = sub.add_parser("list", help="查询在办任务")
2025
2134
  p.add_argument("--mine", action="store_true", help="只看自己的")
2026
2135
  p.add_argument("--owner", help="只看某个人的")
2027
- p.add_argument("--status", help="逗号分隔,如 doing,blocked")
2136
+ p.add_argument("--status", help="逗号分隔,如 doing,open")
2028
2137
  p.add_argument("--category", help="按分类过滤(精确匹配)")
2029
2138
  p.add_argument("--tag", help="按标签过滤(包含匹配,命中任一面即算)")
2030
2139
  p.add_argument(