dsh-task-ledger 0.0.53 → 0.0.55
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/dist/client.js +15 -15
- package/dist/client.js.map +1 -1
- package/dist/host/services/home.service.d.ts +4 -2
- package/dist/host/services/home.service.js +17 -8
- package/dist/host/types.d.ts +14 -10
- package/package.json +1 -1
- package/skill/README.md +19 -12
- package/skill/SKILL.md +85 -132
- package/skill/assets/worklog.md +11 -12
- package/skill/package.json +3 -2
- package/skill/references/log-note.md +47 -0
- package/skill/references/state-model.md +43 -0
- package/skill/references/taxonomy.md +26 -0
- package/skill/references/troubleshooting.md +26 -0
- package/skill/references/worklog.md +37 -0
- package/skill/scripts/progress.py +144 -35
|
@@ -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"
|
|
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
|
|
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
|
|
1296
|
-
raise Fail(
|
|
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
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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,
|
|
1543
|
-
|
|
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,
|
|
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(
|