dsh-task-ledger 0.0.98 → 0.0.100

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-task-ledger",
3
- "version": "0.0.98",
3
+ "version": "0.0.100",
4
4
  "description": "Task ledger board for dsh (DeepSeek Harness): read-only board over task-ledger skill workspaces. The task-ledger skill is bundled — install the plugin, get the skill globally.",
5
5
  "keywords": [
6
6
  "deepseek",
package/skill/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: task-ledger
3
3
  description: 本地任务台账,跨会话记录在办任务的归属、进度与起止。当用户要登记一件新的事(新建任务 / 建个坑 / 这活我领了)、记录已有任务的进展(接着做 / 记一条进展 / 这轮先记下)、查看任务状态或历史(现在该做什么 / 这活谁在做 / 上周做过什么)、给任务结案或退回(做完了 / 验收通过 / 不做了 / 退回待认领)、整理标签或检查台账一致性时加载;只是改代码、讨论方案时不要用。
4
- version: 1.3.24
4
+ version: 1.3.26
5
5
  allowed-tools: Read,Edit,Bash,Grep
6
6
  ---
7
7
 
@@ -41,7 +41,7 @@ allowed-tools: Read,Edit,Bash,Grep
41
41
 
42
42
  | 参数 | 含义 |
43
43
  |---|---|
44
- | `--title "…"` | 任务标题(必填)—— 写任务核心、一句话说清干啥;它用来生成任务号和过程文件名,建议 4~20 字 |
44
+ | `--title "…"` | 任务标题(必填)—— 写任务核心、一句话说清干啥,不要拿分类名当标题;它用来生成任务号和过程文件名,建议 4~20 字 |
45
45
  | `--id` | 自定义任务号(一般不传;只能用小写字母、数字、汉字和连字符) |
46
46
  | `--created` | 给过去漏记的任务补建时,补录实际创建日期(`YYYY-MM-DD`) |
47
47
  | `--self` | 建并认领:直接归自己名下、状态「进行中」。仅当用户明确说「并开始做 / 并执行」时才用(判定见生命周期第 2 条) |
@@ -49,7 +49,7 @@ allowed-tools: Read,Edit,Bash,Grep
49
49
 
50
50
  干什么:建任务、按模板建好过程文档。不加 `--self` = 只建,落公共池等认领;加 `--self` = 建并认领。
51
51
 
52
- 分类 / 标签口径(`new` 与 `close` 共用这组参数):`--category` 答「是什么类型」,单值,中文 ≤4 字 / 纯英文 ≤3 词,吃不准写「其他」;`--tags` 答「涉及什么」,0~5 个、逗号分隔,写不出就不传(传空串等于没传)。tags 不许写 category 的同义词;查询用 tags(包含匹配,见 `list --tag`)。名词性短语、中文优先。**在办任务的分类 / 标签一旦写下就改不了**,只有结案时用 `close --category` / `--tags` 写终值。正例:`--category 缺陷修复 --tags "镜像拉取,超时重试"`;反例:`--category 已完成登录修复`、`--tags 修 bug`。
52
+ 分类 / 标签口径(`new` / `set` / `close` 共用这组参数):`--category` 答「是什么类型」,单值,中文 ≤4 字 / 纯英文 ≤3 词,吃不准写「其他」;`--tags` 答「涉及什么」,0~5 个、逗号分隔,写不出就不传(传空串等于没传)。tags 不许写 category 的同义词;查询用 tags(包含匹配,见 `list --tag`)。名词性短语、中文优先。**建任务时没写、后来想补就用 `set --category` / `--tags`**(在办任务随时可改);结案时 `close` 写的值会覆盖成终值并进历史行。正例:`--category 缺陷修复 --tags "镜像拉取,超时重试"`;反例:`--category 已完成登录修复`、`--tags 修 bug`。
53
53
 
54
54
  举例:用户说「新建个任务:把发布流程走通」→ `new --title "把发布流程走通"`(只建,不认领);用户明确说「新建个任务并开始做」→ `new --title "把发布流程走通" --self`。
55
55
 
@@ -63,17 +63,24 @@ allowed-tools: Read,Edit,Bash,Grep
63
63
 
64
64
  举例:用户说「这活我领了」→ `claim --id 20261023-1117-把发布流程走通-7f3a`。
65
65
 
66
- ### set — 释放任务(退回公共池)
66
+ ### set — 释放任务 / 改分类标签
67
67
 
68
68
  | 参数 | 含义 |
69
69
  |---|---|
70
- | `--id <任务号>` | 释放哪个(必填) |
70
+ | `--id <任务号>` | 哪条任务(必填) |
71
71
  | `--release` | 释放回公共池(不带人) |
72
72
  | `--owner <别人的uuid>` | 把任务转交给对方(预留通道,**agent 不用**,见纪律;只收 uuid,传名字当场报错) |
73
+ | `--category <分类>` | 改分类(改过程文档头部「分类/标签」行 + 分片字段) |
74
+ | `--tags <a、b>` | 改标签(顿号 / 逗号分隔,最多 5 个) |
73
75
 
74
- 干什么:把任务放回公共池、状态回「待认领」,别人即可认领;没有单独的「改状态」入口。**`--release` 与 `--owner` 必须二选一、不能同给**(只给 `--id` 会报错)。
76
+ 干什么:两组用法,**不能混在一次调用里** ——
75
77
 
76
- 举例:用户明确说「把这个任务退回去」→ `set --id <任务号> --release`。
78
+ - `--release` / `--owner`:改归属。释放回公共池、状态回「待认领」,别人即可认领;没有单独的「改状态」入口。**这两个必须二选一、不能同给**。
79
+ - `--category` / `--tags`:改分类标签。只改自己名下**在办**任务;给了哪个就替换哪个,没给的沿用原值;头部那行没有就插到最上面,有就原地改。已结案(在历史里)的任务改不了。
80
+
81
+ 只给 `--id` 会报错,要求你指明要做哪件事。
82
+
83
+ 举例:用户明确说「把这个任务退回去」→ `set --id <任务号> --release`;「这任务归到建设类、打上台账标签」→ `set --id <任务号> --category 建设 --tags 台账、看板`。
77
84
 
78
85
  ### log — 记一条进展
79
86
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-ledger",
3
- "version": "1.3.24",
3
+ "version": "1.3.26",
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",
@@ -30,7 +30,7 @@ close → 历史层 + 封卷 + 移出自己名下
30
30
  - 进门:`--since` / `--until` / `--created` / `--finished` 按本机墙钟给,脚本内部转 UTC 再比较 / 落盘;除这四个参数没有任何地方要调用方传时间。
31
31
  - **过程文档正文不动**:`show --json` 的 `worklog.text` 是原文,里面的时间是当时写进去的 UTC。
32
32
  - 存量老数据写的是写入者本地时间、无标记,无法与 UTC 区分(JSON 是 2026-10-22 前,执行记录行是 2026-10-27 前)。
33
- - **history 文件 11 列**:结案时间 / 创建时间 / 工作包(= `new --title` 的任务标题)/ 分类 / 标签 / owner / owner_name / creator / creator_name / 摘要 / 过程文档;排序与 `--since` / `--until` 按**结案时间**。(`history` 命令的人话输出只展示其中几列:结案时间、负责人、标题。)
33
+ - **history 文件 11 列**:结案时间 / 创建时间 / 标题 / 分类 / 标签 / owner / owner_name / creator / creator_name / 摘要 / 过程文档;排序与 `--since` / `--until` 按**结案时间**。(`history` 命令的人话输出只展示其中几列:结案时间、负责人、标题。)
34
34
  - **owner / creator 两列存的是 uuid**(与任务里一致,身份只认 uuid);紧随其后的 `owner_name` / `creator_name` 两列**落盘**结案那一刻的显示名 —— 改名不回写旧行,看历史看到的就是当时那个名字。老行(升级前落的)没有名字时按名册兜底补一次展示值,**不回写文件**。
35
35
 
36
36
  ## 任务号
@@ -43,7 +43,7 @@ ACTIONS: dict[str, ActionSpec] = {
43
43
  "set": ActionSpec(
44
44
  set_task.set_task,
45
45
  "result",
46
- params=("tid", "owner", "release"),
46
+ params=("tid", "owner", "release", "category", "tags"),
47
47
  lock_action="set",
48
48
  ),
49
49
  "log": ActionSpec(log_progress.log_progress, "result", params=("tid", "note")),
@@ -0,0 +1,105 @@
1
+ """改分类 / 标签:分片那两个字段 + 过程文档头部那行,一次改动两处同步。
2
+
3
+ 只动「分类 / 标签」这一件事,不碰归属与状态 —— 归属走 `--owner` / `--release`,
4
+ 状态只随命令语义变。已结案的任务不在任何分片里,改不了(历史行是快照)。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from ..core.identity import Identity
12
+ from ..core.labels import normalize_label, parse_tags
13
+ from ..core.paths import shard_rel
14
+ from ..core.time import now
15
+ from ..core.worklog_md import (
16
+ StatusLine,
17
+ append_exec_log,
18
+ exec_line,
19
+ rewrite_label_line,
20
+ rewrite_status_line,
21
+ )
22
+ from ..errors import Fail
23
+ from ..store.task_store import TaskStore
24
+ from ..store.worklog_store import WorklogStore
25
+ from .guard import require_owner
26
+ from .result import Result, rel
27
+
28
+
29
+ def _describe(category: str | None, tags: list[str]) -> str:
30
+ """执行记录里那一句说明:`建设 · DSH、台账`。"""
31
+ parts = [category] if category else []
32
+ if tags:
33
+ parts.append("、".join(tags))
34
+ return " · ".join(parts)
35
+
36
+
37
+ def relabel(
38
+ repo: Path,
39
+ me: Identity,
40
+ tid: str,
41
+ *,
42
+ category: str | None = None,
43
+ tags: str | tuple[str, ...] | None = None,
44
+ ) -> Result:
45
+ """写序:算最终值 → 过程文档(标签行 + 执行记录 + 状态行,一次写)→ 分片。
46
+
47
+ `tags` 收字符串(顿号 / 逗号分隔)或已切好的元组 —— 外壳可能已经切过一遍。
48
+ """
49
+ new_category = normalize_label(category, field="分类") if category else None
50
+ new_tags = parse_tags(tags) if tags else None
51
+ if new_category is None and new_tags is None:
52
+ raise Fail(
53
+ "改分类 / 标签至少要给一个:`--category <分类>` 或 `--tags <a、b>`。"
54
+ )
55
+
56
+ mine = TaskStore(repo, shard_rel(me.uuid), identity=me)
57
+ task = mine.get(tid) # 锁内重读
58
+ if task is None:
59
+ raise Fail(
60
+ f"任务 {tid} 不在我的分片里(只能改自己名下在办的任务;已结案的任务改不了)。"
61
+ )
62
+ require_owner(task, tid, me)
63
+
64
+ # 没给的沿用原值:分类与标签是一行里的两段,写一次就得给全
65
+ final_category = new_category if new_category else task.get("category")
66
+ final_tags: list[str] = (
67
+ [str(tag) for tag in new_tags]
68
+ if new_tags is not None
69
+ else [str(tag) for tag in (task.get("tags") or ())]
70
+ )
71
+ if not final_category and not final_tags:
72
+ raise Fail("分类与标签都是空的,没有可写的值。")
73
+
74
+ when = now()
75
+ updated = dict(task)
76
+ updated["category"] = final_category
77
+ updated["tags"] = final_tags
78
+ updated["updated"] = when
79
+
80
+ logs = WorklogStore(repo)
81
+ path = logs.find(tid, task.get("worklog"))
82
+ if path is None:
83
+ mine.put(tid, updated)
84
+ return Result(
85
+ changed=("category", "tags", "updated"),
86
+ where=rel(repo, mine.path),
87
+ message=f"{tid}:改分类 / 标签",
88
+ notices=("提示:这条任务没有过程文档,只改了分片,文档与历史里看不到。",),
89
+ )
90
+
91
+ text = logs.read(path)
92
+ note = "改分类 / 标签:" + _describe(final_category, final_tags)
93
+ text = rewrite_label_line(text, final_category, final_tags)
94
+ text = append_exec_log(text, exec_line(when, me.stamp(), "set", note))
95
+ text = rewrite_status_line(
96
+ text, StatusLine(task.get("status") or "doing", me.name, me.uuid, when)
97
+ )
98
+ logs.write(path, text)
99
+ mine.put(tid, updated)
100
+
101
+ return Result(
102
+ changed=("category", "tags", "updated"),
103
+ where=rel(repo, mine.path),
104
+ message=f"{tid}:改分类 / 标签",
105
+ )
@@ -18,10 +18,16 @@ from ..errors import Fail
18
18
  from ..store.task_store import TaskStore
19
19
  from . import claim as claim_mod
20
20
  from . import handoff as handoff_mod
21
+ from . import relabel as relabel_mod
21
22
  from . import release as release_mod
22
23
  from .guard import require_uuid
23
24
  from .result import Result, rel
24
25
 
26
+ _MIXED = (
27
+ "--category / --tags 不能与 --release / --owner 同时给:"
28
+ "一个是改分类标签,一个是改归属,分两次跑。"
29
+ )
30
+
25
31
 
26
32
  def set_task(
27
33
  repo: Path,
@@ -30,15 +36,28 @@ def set_task(
30
36
  *,
31
37
  owner: str | None = None,
32
38
  release: bool = False,
39
+ category: str | None = None,
40
+ tags: str | tuple[str, ...] | None = None,
33
41
  ) -> Result:
34
- """路由:`--release` = 释放;别人的 uuid = 交接;自己的 uuid = 已认领则空转、没认领先认领。"""
42
+ """路由:`--release` = 释放;别人的 uuid = 交接;自己的 uuid = 已认领则空转、没认领先认领;
43
+ `--category` / `--tags` = 改分类与标签(只动文档头部那行与分片字段)。
44
+ """
45
+ relabel_wanted = bool(category or tags)
35
46
  if release:
36
47
  if owner is not None:
37
48
  raise Fail("--release 与 --owner 不能同时给:释放不带人,交接不带释放。")
49
+ if relabel_wanted:
50
+ raise Fail(_MIXED)
38
51
  return release_mod.release(repo, me, tid)
39
- if owner is None:
52
+ if owner is not None:
53
+ if relabel_wanted:
54
+ raise Fail(_MIXED)
55
+ else:
56
+ if relabel_wanted:
57
+ return relabel_mod.relabel(repo, me, tid, category=category, tags=tags)
40
58
  raise Fail(
41
- "set 必须二选一:`--release`(释放回公共池)或 `--owner <对方 uuid>`(交接)。"
59
+ "set 必须给一样:`--release`(释放回公共池)/ `--owner <对方 uuid>`(交接)/ "
60
+ "`--category <分类>` 或 `--tags <a、b>`(改分类标签)。"
42
61
  )
43
62
  # 交接目标只收 uuid:名字查不到就当场报错,绝不去猜「他大概是这个人」
44
63
  target = require_uuid(owner, field="--owner")
@@ -7,7 +7,7 @@
7
7
  逐篇读全文换不来体检结论,只白花时间;文档自身的格式与内容不在体检范围。
8
8
 
9
9
  只查格式,不查业务(该谁结案、逾期与否都不管);也不查「同一任务号落在多个文件」——
10
- 读侧只显示一次(`show` 会点名)、写侧已有防护,撞上了不关体检的事(2026-10-24 用户拍板)。
10
+ 读侧只显示一次(`list` / `show` 都不重复)、写侧已有防护,撞上了不关体检的事(2026-10-24 用户拍板)。
11
11
  """
12
12
 
13
13
  from __future__ import annotations
@@ -15,7 +15,7 @@ from .time import TIME_RE, time_key
15
15
 
16
16
  HISTORY_TITLE = "# 进度历史(已结案)"
17
17
  HISTORY_HEADER = (
18
- "| 结案时间 | 创建时间 | 工作包 | 分类 | 标签 "
18
+ "| 结案时间 | 创建时间 | 标题 | 分类 | 标签 "
19
19
  "| owner | owner_name | creator | creator_name | 完成了什么 | 过程文档 |"
20
20
  )
21
21
  HISTORY_SEP = "|---|---|---|---|---|---|---|---|---|---|---|"
@@ -45,10 +45,14 @@ def build_parser() -> argparse.ArgumentParser:
45
45
  claim = sub.add_parser("claim", help="认领公共池里的任务")
46
46
  claim.add_argument("--id", required=True, dest="tid")
47
47
 
48
- setp = sub.add_parser("set", help="释放 / 交接任务")
48
+ setp = sub.add_parser("set", help="释放 / 交接 / 改分类标签")
49
49
  setp.add_argument("--id", required=True, dest="tid")
50
50
  setp.add_argument("--owner", help="交给别人的 uuid(交接)")
51
51
  setp.add_argument("--release", action="store_true", help="释放回公共池")
52
+ setp.add_argument(
53
+ "--category", help="改分类(与 --tags 一起改过程文档头部分类/标签行)"
54
+ )
55
+ setp.add_argument("--tags", help="改标签,顿号 / 逗号分隔")
52
56
 
53
57
  logp = sub.add_parser("log", help="记一条进展")
54
58
  logp.add_argument("--id", required=True, dest="tid")
@@ -53,7 +53,7 @@ class HistoryStore:
53
53
  ) -> str:
54
54
  """这个月的历史里已经有这条了吗?三态:`hit` / `miss` / `undecidable`。
55
55
 
56
- 有末列就比**文件名**(不比整条路径)。末列为空的行改按「创建时间 + 工作包」两列比
56
+ 有末列就比**文件名**(不比整条路径)。末列为空的行改按「创建时间 + 标题」两列比
57
57
  —— 那种行在调用方给不出 `created` / `title` 时判不了,明说 `undecidable`,不拿 miss 冒充。
58
58
  """
59
59
  records, _ = history_md.parse_history(self.read(path))