task-ledger 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -8
- package/SKILL.md +11 -8
- package/assets/worklog.md +7 -0
- package/package.json +1 -1
- package/scripts/progress.py +132 -33
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
### 状态用 JSON,不用 Markdown
|
|
37
37
|
|
|
38
|
-
状态要频繁改(`
|
|
38
|
+
状态要频繁改(`open` → `doing` → 结案),Markdown 里改一个词很容易顺手改到别处。JSON 结构化、字段固定、一次只动一个值,改动面最小。
|
|
39
39
|
|
|
40
40
|
### 状态与过程正文分离
|
|
41
41
|
|
|
@@ -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
|
|
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,15 +187,13 @@ stateDiagram-v2
|
|
|
187
187
|
[*] --> open: new(不带 --self)
|
|
188
188
|
[*] --> doing: new --self
|
|
189
189
|
open --> doing: claim(认领,写 owner)
|
|
190
|
-
doing -->
|
|
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
|
|
package/SKILL.md
CHANGED
|
@@ -14,11 +14,11 @@ allowed-tools: Read,Write,Bash,Grep
|
|
|
14
14
|
| 文件 | 装什么 | 谁写 |
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| `ledger/progress.json` | 在办任务的状态:任务号 / 标题 / 创建人 / owner / 状态 / 时间 / **分类(`category`)与标签(`tags`)** / 过程文档路径(`worklog` 字段) | **只由脚本写** |
|
|
17
|
-
| `ledger/worklog/<年月>/<日>-<时分>-<说明>-<随机码>.md` |
|
|
17
|
+
| `ledger/worklog/<年月>/<日>-<时分>-<说明>-<随机码>.md` | 该任务的过程正文,固定六节:背景 / 待办 / 下一步 / 过程 / 结论 + **执行记录**(末节,脚本维护、禁手改) | **建档只由脚本按模板做**,之后正文(前五节)由 agent 直接编辑 |
|
|
18
18
|
| `ledger/history/<年月>.md` | 已结案任务,一行一条(8 列:结案时间 / 创建时间 / 工作包 / 分类 / 标签 / **owner** / 摘要 / 过程文档);按**结案时间**分月。存量 `ledger/PROGRESS-HISTORY.md` 只读兼容、永不重写 | **只由脚本写**;查询走 `history` 子命令 |
|
|
19
19
|
|
|
20
20
|
这三个文件之外不要另建副本。**不要直接编辑状态表和历史索引 —— 一律走脚本。**
|
|
21
|
-
过程文档的**建档也一律走脚本**(`new
|
|
21
|
+
过程文档的**建档也一律走脚本**(`new`):脚本按模板生成六节骨架、把路径写进状态表;
|
|
22
22
|
你只负责往里填正文,**不要自己拼文件名、也不要另建一个 md 顶替它**。
|
|
23
23
|
|
|
24
24
|
**台账 ≠ 产出物**:台账(`ledger/`)只装「什么时候建 / 谁在做 / 做到哪 / 什么时候结案」
|
|
@@ -49,14 +49,14 @@ allowed-tools: Read,Write,Bash,Grep
|
|
|
49
49
|
|---|---|---|
|
|
50
50
|
| 「现在该做什么」「我还有啥没做完」 | 查自己的在办 | `list --mine` |
|
|
51
51
|
| 「谁在做 X」「有哪些在办任务」 | 查全部在办 | `list` |
|
|
52
|
-
|
|
|
52
|
+
| 「有哪些在等认领」 | 按状态查 | `list --status open` |
|
|
53
53
|
| 「某件老活是什么时候做的」 | 查已结案 | `history`(只读,可直接跑) |
|
|
54
54
|
| 「谁改过什么」「拉某人的工作日志」 | 按人查已结案 | `history --owner "<名字>"`(**精确匹配**) |
|
|
55
55
|
| 「这周 / 这个月建了啥活」 | 按创建时间查已结案 | `history --since 2026-10-01` |
|
|
56
56
|
| 「新建一个任务」「我要做 X」 | 建任务 | `new` |
|
|
57
57
|
| 「这活我领了」「我来做这个」 | 认领 | `claim` |
|
|
58
|
-
| 「卡住了」「先放一放」 |
|
|
59
|
-
|
|
|
58
|
+
| 「卡住了」「先放一放」 | 写清卡在哪(不改状态) | 编辑 worklog 的「待办 / 过程」,再 `log --note "卡在…"` |
|
|
59
|
+
| 「继续做」 | 记一条推进 | `log --note "继续推进…"` |
|
|
60
60
|
| 「转给某人」「交接给某人」 | 换负责人 | `set --owner "<新负责人>"` |
|
|
61
61
|
| 「这活做完了」「验收通过」「结案」 | 结案归档 | `close`(**这句必须由用户说**) |
|
|
62
62
|
| 项目里还没有状态表 | 初始化 | `init` |
|
|
@@ -80,6 +80,7 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <当
|
|
|
80
80
|
| `new` | 新建任务,并自动建过程文档 | `--title`(必填)、`--id`(可选)、`--self`、`--created`(可选,补录创建时间)、`--category` / `--tags`(可选,分类 / 标签,见下节口径) |
|
|
81
81
|
| `claim` | 认领一个待认领任务 | `--id`(必填) |
|
|
82
82
|
| `set` | 改状态 / 换负责人 | `--id`(必填)、`--status`、`--owner` |
|
|
83
|
+
| `log` | 往 worklog 的「执行记录」节补一条 | `--id`(必填)、`--note`(必填,一句话) |
|
|
83
84
|
| `close` | 结案:移出状态 + 追加历史 + 封卷过程文档 | `--id`、`--summary`(都必填)、`--finished`(可选,结案时间)、`--category` / `--tags`(可选,结案时覆盖修正一次) |
|
|
84
85
|
| `list` | 查询在办任务 | `--mine`、`--owner`、`--status`、`--since`、`--until`、`--category`(精确)、`--tag`(包含)、`--json` |
|
|
85
86
|
| `history` | 查已结案任务(只读) | `--owner`、`--mine`、`--since`、`--until`、`--limit`、`--category`(精确)、`--tag`(包含)、`--json`;另:`--count-only`(只数行数、封顶 1 万输出 `{count, capped}`,显示级统计)、`--months N`(只读最近 N 个月文件,不含存量平铺文件) |
|
|
@@ -92,13 +93,15 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <当
|
|
|
92
93
|
| 命令 | 返回 |
|
|
93
94
|
|---|---|
|
|
94
95
|
| `new` / `claim` / `set` / `show` | 状态行 + **该任务过程文档的路径与全文** —— 拿到就能直接编辑,不用再去找文件、拼路径(`history` 是例外:只给链接不给全文) |
|
|
96
|
+
| `log` | 一行确认(记了什么) |
|
|
95
97
|
| `close` | 结案结果与摘要 |
|
|
96
98
|
| `list` | 在办任务列表(**不含**文档正文,避免撑爆上下文) |
|
|
97
99
|
| `history` | 已结案紧凑列表(结案时间 / 创建时间 / owner / 工作包 / 过程文档链接,行尾附所在历史文件),**默认不含摘要**;要摘要走 `--json` |
|
|
98
100
|
|
|
99
101
|
### 几个必须知道的点
|
|
100
102
|
|
|
101
|
-
-
|
|
103
|
+
- **状态取值**:只有 `open`(已建待认领)/ `doing`(已认领在做)两个;`done` 只能由 `close` 产生,不要用 `set --status done`。
|
|
104
|
+
**没有 `blocked` 状态**:阻塞由时间判定(接任务满 7 天仍未结案),看板自己算,不用也不能手动改;卡在哪里写进 worklog。旧数据里残留的 `blocked` 读侧当进行中处理(`list --status blocked` 可捞出来,用 `set --status doing` 归位)。
|
|
102
105
|
- **`new --self`** = 建完即认领(直接 `doing`);**不带 `--self`** = 空开,等别人 `claim`。
|
|
103
106
|
- **任务号自动生成**:创建时间(`年月日-时分`)+ 标题里的字母、数字、汉字 + 4 位随机码(如 `20261015-1430-登录接入-sso-7f3a`)。想指定就传 `--id`。
|
|
104
107
|
- **owner 取 `git config user.name`**:认领人**只能是当前 git 身份,不能由外部传入**;读不到就直接报错退出(提示「需要用户名才能执行」),不做任何兜底 —— 先让用户配好 git 身份。(`history` 只有 `--mine` 会取这个身份;`--owner <别人>` 是只读查询,不校验身份)
|
|
@@ -117,7 +120,7 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <当
|
|
|
117
120
|
## 过程文档的生命周期
|
|
118
121
|
|
|
119
122
|
1. **建档** —— 执行 `new` 时,脚本**必须**按 `assets/worklog.md` 模板创建 `ledger/worklog/<年月>/<日>-<时分>-<说明>-<随机码>.md`(`<年月>` 取任务创建月份,如 `202610`;年月已经在目录名里,文件名不再重复)。路径同时写进状态表的 `worklog` 字段,查的时候不必靠推算。
|
|
120
|
-
2. **推进** —— 这个 md 由 **agent
|
|
123
|
+
2. **推进** —— 这个 md 由 **agent 直接编辑**(它是正文,不是状态数据,不走脚本)。六节(背景 / 待办 / 下一步 / 过程 / 结论 / 执行记录)**标题结构不能动**,且**末节「执行记录」由脚本维护、不许手改**:改完前五节内容后跑一次 `log --id <任务号> --note "这轮做了什么"`,脚本会按时间追加一行(`new` / `claim` / `set` / `close` 也会各自自动记一条)。记录格式固定 `- 时间 · 类型 · @谁 · 说明`,类型取 `new` / `claim` / `set` / `release`(`set --status open` 释放认领,单列)/ `log` / `close`;**只追加不改旧行**,移交后旧记录仍记在原负责人名下。**各写什么、写成什么样算对,模板注释里给了正例**,照它填即可。
|
|
121
124
|
3. **交付(待验收)** —— 活干完了但**用户还没验收**时,任务仍留在 `doing`:worklog 的「待办」写「待用户验收」,「下一步」写清验收该看什么(哪条命令、哪个文件)。**这一段不产出状态变化,只写正文。**
|
|
122
125
|
4. **封卷** —— 执行 `close` 时,脚本自动打封卷标记并把头部状态改为完成,此后不再修改。历史行按**结案时间**落 `ledger/history/<年月>.md`(存量 `ledger/PROGRESS-HISTORY.md` 只读、不再写入);封卷后要找回这篇文档:`history` 会给出它的链接与所在历史文件(只读、无需确认)。
|
|
123
126
|
|
|
@@ -149,7 +152,7 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <当
|
|
|
149
152
|
|
|
150
153
|
`init` / `new` / `claim` / `set` / `close` 都有副作用,但**分两档**:
|
|
151
154
|
|
|
152
|
-
**一、动手类(`init` / `new` / `claim` / `set`)**:执行前先把完整参数复述给用户并取得确认(任务号、标题、负责人、目标状态)。不得先执行再补报告,也不得一次执行多条用户没逐条确认的写操作。
|
|
155
|
+
**一、动手类(`init` / `new` / `claim` / `set` / `log`)**:执行前先把完整参数复述给用户并取得确认(任务号、标题、负责人、目标状态)。不得先执行再补报告,也不得一次执行多条用户没逐条确认的写操作。
|
|
153
156
|
|
|
154
157
|
**二、`close` 是独立授权,不是做任务流程的最后一步**:
|
|
155
158
|
|
package/assets/worklog.md
CHANGED
|
@@ -29,3 +29,10 @@
|
|
|
29
29
|
|
|
30
30
|
<!-- 收尾时填:做成了什么、留了什么坑。
|
|
31
31
|
例:已修复,但只覆盖 UTF-8;GBK 另开任务 -->
|
|
32
|
+
|
|
33
|
+
## 执行记录
|
|
34
|
+
|
|
35
|
+
<!-- 本节由脚本维护:new / claim / set / close / log 自动追加,请勿手改。
|
|
36
|
+
要补一条就跑 progress.py log --id <任务号> --note "做了什么"。
|
|
37
|
+
每条一行:时间 · 类型 · @谁 · 说明(类型取 new / claim / set / release / log / close);
|
|
38
|
+
只追加、不改旧行 —— 移交后旧记录仍留在原负责人名下。 -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-ledger",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
package/scripts/progress.py
CHANGED
|
@@ -37,7 +37,10 @@ try:
|
|
|
37
37
|
except ImportError:
|
|
38
38
|
msvcrt = None
|
|
39
39
|
|
|
40
|
-
STATUSES = ("open", "doing"
|
|
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")
|
|
@@ -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,53 @@ 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
|
+
"""把一条记录追加到「执行记录」节末尾;文档没有这一节(老 worklog)原样返回。"""
|
|
1162
|
+
lines = text.splitlines()
|
|
1163
|
+
at = next((i for i, ln in enumerate(lines) if ln.strip() == EXEC_HEAD), None)
|
|
1164
|
+
if at is None:
|
|
1165
|
+
return text
|
|
1166
|
+
end = next(
|
|
1167
|
+
(i for i in range(at + 1, len(lines)) if lines[i].startswith("## ")), len(lines)
|
|
1168
|
+
)
|
|
1169
|
+
insert = end
|
|
1170
|
+
while insert > at + 1 and not lines[insert - 1].strip():
|
|
1171
|
+
insert -= 1
|
|
1172
|
+
lines.insert(insert, line)
|
|
1173
|
+
return "\n".join(lines) + "\n"
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def record_exec(
|
|
1177
|
+
repo: Path, tid: str, task: dict[str, Any], who: str | None, kind: str, note: str
|
|
1178
|
+
) -> None:
|
|
1179
|
+
"""给任务的过程文档补一条执行记录(`who` = 这次动手的人,不是任务现 owner)。
|
|
1180
|
+
|
|
1181
|
+
调用方必须已经持有 progress_lock —— 与 close 的封卷(读全文→改写→落盘)互斥,
|
|
1182
|
+
否则两条记录并发追加会互相覆盖。
|
|
1183
|
+
"""
|
|
1184
|
+
log = find_worklog(repo, tid, task)
|
|
1185
|
+
if log is None:
|
|
1186
|
+
return
|
|
1187
|
+
try:
|
|
1188
|
+
text = log.read_text(encoding="utf-8")
|
|
1189
|
+
except (OSError, UnicodeDecodeError):
|
|
1190
|
+
return
|
|
1191
|
+
line = exec_line(now(), who, kind, note)
|
|
1192
|
+
new_text = append_exec_log(text, line)
|
|
1193
|
+
if new_text != text:
|
|
1194
|
+
write_text_atomic(log, new_text)
|
|
1195
|
+
|
|
1196
|
+
|
|
1137
1197
|
def write_worklog(repo: Path, tid: str, task: dict[str, Any]) -> Path:
|
|
1138
1198
|
log = repo / worklog_dir_for(task) / worklog_name(tid)
|
|
1139
1199
|
write_text_atomic(log, render_worklog(tid, task))
|
|
@@ -1218,6 +1278,7 @@ def cmd_new(args: argparse.Namespace) -> None:
|
|
|
1218
1278
|
"status": "doing" if args.self else "open",
|
|
1219
1279
|
"created": stamp,
|
|
1220
1280
|
"started": stamp if args.self else None,
|
|
1281
|
+
"updated": stamp,
|
|
1221
1282
|
"finished": None,
|
|
1222
1283
|
"category": category,
|
|
1223
1284
|
"tags": tags,
|
|
@@ -1236,6 +1297,7 @@ def cmd_new(args: argparse.Namespace) -> None:
|
|
|
1236
1297
|
except Fail:
|
|
1237
1298
|
pass
|
|
1238
1299
|
raise Fail(f"创建失败,已从状态表撤销该任务:{exc}")
|
|
1300
|
+
record_exec(repo, tid, task, creator, "new", f"创建任务({task['status']})")
|
|
1239
1301
|
print(f"已创建任务 {tid}")
|
|
1240
1302
|
print(f" 标题 {task['title']}")
|
|
1241
1303
|
print(f" 状态 {task['status']}")
|
|
@@ -1271,10 +1333,13 @@ def cmd_claim(args: argparse.Namespace) -> None:
|
|
|
1271
1333
|
f"任务 {tid} 当前状态是 {task.get('status')},只有 open 才能认领。"
|
|
1272
1334
|
)
|
|
1273
1335
|
|
|
1336
|
+
stamp = now()
|
|
1274
1337
|
task["owner"] = owner
|
|
1275
1338
|
task["status"] = "doing"
|
|
1276
|
-
task["started"] = task.get("started") or
|
|
1339
|
+
task["started"] = task.get("started") or stamp
|
|
1340
|
+
task["updated"] = stamp
|
|
1277
1341
|
commit_guarded(path, before, data)
|
|
1342
|
+
record_exec(repo, tid, task, owner, "claim", "认领 → doing")
|
|
1278
1343
|
print(f"已认领 {tid} → doing,owner = @{owner}")
|
|
1279
1344
|
print_worklog(repo, tid, task)
|
|
1280
1345
|
|
|
@@ -1292,12 +1357,17 @@ def cmd_set(args: argparse.Namespace) -> None:
|
|
|
1292
1357
|
raise Fail(
|
|
1293
1358
|
"--status open(退回未认领)不能同时给 --owner;要交给别人就别改回 open。"
|
|
1294
1359
|
)
|
|
1295
|
-
if status
|
|
1296
|
-
raise Fail(
|
|
1360
|
+
if status == "blocked":
|
|
1361
|
+
raise Fail(
|
|
1362
|
+
"没有 blocked 状态:阻塞由时间自动判定(接任务满 7 天仍未结案),"
|
|
1363
|
+
"状态只有 open / doing;卡在哪里请写进 worklog。"
|
|
1364
|
+
)
|
|
1297
1365
|
if status == "done":
|
|
1298
1366
|
raise Fail(
|
|
1299
1367
|
"不要把状态直接改成 done —— 用 close 结案(它会同时移出状态、追加历史、封卷 worklog)。"
|
|
1300
1368
|
)
|
|
1369
|
+
if status is not None and status not in STATUSES:
|
|
1370
|
+
raise Fail(f"--status 只能是 {' / '.join(STATUSES)};结案请用 close。")
|
|
1301
1371
|
|
|
1302
1372
|
repo = find_repo(args.repo)
|
|
1303
1373
|
tid = require_task_id(args.id)
|
|
@@ -1328,11 +1398,35 @@ def cmd_set(args: argparse.Namespace) -> None:
|
|
|
1328
1398
|
task["started"] = task.get("started") or now()
|
|
1329
1399
|
changed.append(f"owner {old or '(无)'} → {task['owner']}")
|
|
1330
1400
|
|
|
1401
|
+
task["updated"] = now()
|
|
1331
1402
|
commit_guarded(path, before, data)
|
|
1403
|
+
# 释放认领(退回 open)单列一个类型,便于一眼看出「被释放过」
|
|
1404
|
+
kind = "release" if status == "open" else "set"
|
|
1405
|
+
record_exec(repo, tid, task, me, kind, ",".join(changed))
|
|
1332
1406
|
print(f"已更新 {tid}:" + ",".join(changed))
|
|
1333
1407
|
print_worklog(repo, tid, task)
|
|
1334
1408
|
|
|
1335
1409
|
|
|
1410
|
+
def cmd_log(args: argparse.Namespace) -> None:
|
|
1411
|
+
repo = find_repo(args.repo)
|
|
1412
|
+
tid = require_task_id(args.id)
|
|
1413
|
+
note = " ".join(args.note.split())
|
|
1414
|
+
if not note:
|
|
1415
|
+
raise Fail("--note 不能为空:一句话写清这轮做了什么。")
|
|
1416
|
+
me = git_user()
|
|
1417
|
+
path = repo / PROGRESS_REL
|
|
1418
|
+
with progress_lock(path):
|
|
1419
|
+
before, data = read_guarded(path)
|
|
1420
|
+
task = data["tasks"].get(tid)
|
|
1421
|
+
if task is None:
|
|
1422
|
+
raise Fail(f"没有任务 {tid}(可能未建或已结案;封卷后不再记)。")
|
|
1423
|
+
require_owner(task, tid, me)
|
|
1424
|
+
task["updated"] = now()
|
|
1425
|
+
commit_guarded(path, before, data)
|
|
1426
|
+
record_exec(repo, tid, task, me, "log", note)
|
|
1427
|
+
print(f"已记入 {tid} 的执行记录:{note}")
|
|
1428
|
+
|
|
1429
|
+
|
|
1336
1430
|
def cmd_close(args: argparse.Namespace) -> None:
|
|
1337
1431
|
repo = find_repo(args.repo)
|
|
1338
1432
|
tid = require_task_id(args.id)
|
|
@@ -1414,29 +1508,33 @@ def cmd_close(args: argparse.Namespace) -> None:
|
|
|
1414
1508
|
if close_tags is not None
|
|
1415
1509
|
else list(task.get("tags") or []),
|
|
1416
1510
|
)
|
|
1417
|
-
new_log = stamp_frozen(log_text, finished, owner)
|
|
1418
1511
|
else:
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
else list(task.get("tags") or [])
|
|
1434
|
-
),
|
|
1512
|
+
log_text = render_worklog(
|
|
1513
|
+
tid,
|
|
1514
|
+
dict(
|
|
1515
|
+
task,
|
|
1516
|
+
status="done",
|
|
1517
|
+
category=(
|
|
1518
|
+
close_category
|
|
1519
|
+
if close_category is not None
|
|
1520
|
+
else task.get("category")
|
|
1521
|
+
),
|
|
1522
|
+
tags=(
|
|
1523
|
+
close_tags
|
|
1524
|
+
if close_tags is not None
|
|
1525
|
+
else list(task.get("tags") or [])
|
|
1435
1526
|
),
|
|
1436
1527
|
),
|
|
1437
|
-
finished,
|
|
1438
|
-
owner,
|
|
1439
1528
|
)
|
|
1529
|
+
# 结案本身是最后一个节点:封卷前补上这条记录,封卷后不再追加
|
|
1530
|
+
new_log = stamp_frozen(
|
|
1531
|
+
append_exec_log(
|
|
1532
|
+
log_text,
|
|
1533
|
+
exec_line(finished, me, "close", f"结案:{summary}"),
|
|
1534
|
+
),
|
|
1535
|
+
finished,
|
|
1536
|
+
owner,
|
|
1537
|
+
)
|
|
1440
1538
|
|
|
1441
1539
|
# 落盘前复查:三个文件都没被别人动过
|
|
1442
1540
|
for p, mark in (
|
|
@@ -1539,18 +1637,13 @@ def cmd_list(args: argparse.Namespace) -> None:
|
|
|
1539
1637
|
if args.status is not None:
|
|
1540
1638
|
wanted = [s.strip() for s in args.status.split(",") if s.strip()]
|
|
1541
1639
|
if not wanted:
|
|
1542
|
-
raise Fail("--status 不能为空;逗号分隔,如 doing,
|
|
1543
|
-
|
|
1640
|
+
raise Fail("--status 不能为空;逗号分隔,如 doing,open")
|
|
1641
|
+
# blocked 只用于捞旧数据(新任务不可能再有这个值)
|
|
1642
|
+
bad = [s for s in wanted if s not in STATUSES and s not in LEGACY_STATUSES]
|
|
1544
1643
|
if bad:
|
|
1545
1644
|
raise Fail(
|
|
1546
1645
|
f"--status 含非法值 {', '.join(bad)};可选 {' / '.join(STATUSES)}"
|
|
1547
1646
|
)
|
|
1548
|
-
if "done" in wanted:
|
|
1549
|
-
print(
|
|
1550
|
-
"提示:done 的任务已结案并移出状态表,用 history 子命令查"
|
|
1551
|
-
"(已结案在 ledger/history/<年月>.md,存量在 ledger/PROGRESS-HISTORY.md)。",
|
|
1552
|
-
file=sys.stderr,
|
|
1553
|
-
)
|
|
1554
1647
|
|
|
1555
1648
|
since = normalize_time(args.since, field="--since") if args.since else None
|
|
1556
1649
|
until = (
|
|
@@ -1734,7 +1827,7 @@ def cmd_validate(args: argparse.Namespace) -> None:
|
|
|
1734
1827
|
if not task.get("title"):
|
|
1735
1828
|
problems.append(f"{tid}: 缺 title")
|
|
1736
1829
|
status = task.get("status")
|
|
1737
|
-
if status not in STATUSES:
|
|
1830
|
+
if status not in STATUSES and status not in LEGACY_STATUSES:
|
|
1738
1831
|
problems.append(f"{tid}: status={status!r} 非法")
|
|
1739
1832
|
if status == "open" and task.get("owner"):
|
|
1740
1833
|
problems.append(f"{tid}: status=open 却有 owner")
|
|
@@ -2010,6 +2103,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2010
2103
|
add_repo(p)
|
|
2011
2104
|
p.set_defaults(func=cmd_set)
|
|
2012
2105
|
|
|
2106
|
+
p = sub.add_parser("log", help="往 worklog 的「执行记录」节补一条记录")
|
|
2107
|
+
p.add_argument("--id", required=True)
|
|
2108
|
+
p.add_argument("--note", required=True, help="一句话:这轮做了什么")
|
|
2109
|
+
add_repo(p)
|
|
2110
|
+
p.set_defaults(func=cmd_log)
|
|
2111
|
+
|
|
2013
2112
|
p = sub.add_parser("close", help="结案:移出状态 + 追加历史 + 封卷 worklog")
|
|
2014
2113
|
p.add_argument("--id", required=True)
|
|
2015
2114
|
p.add_argument("--summary", required=True, help="做成了什么,一句话")
|
|
@@ -2024,7 +2123,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2024
2123
|
p = sub.add_parser("list", help="查询在办任务")
|
|
2025
2124
|
p.add_argument("--mine", action="store_true", help="只看自己的")
|
|
2026
2125
|
p.add_argument("--owner", help="只看某个人的")
|
|
2027
|
-
p.add_argument("--status", help="逗号分隔,如 doing,
|
|
2126
|
+
p.add_argument("--status", help="逗号分隔,如 doing,open")
|
|
2028
2127
|
p.add_argument("--category", help="按分类过滤(精确匹配)")
|
|
2029
2128
|
p.add_argument("--tag", help="按标签过滤(包含匹配,命中任一面即算)")
|
|
2030
2129
|
p.add_argument(
|