dsh-task-ledger 0.0.97 → 0.0.98

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 CHANGED
@@ -4883,8 +4883,8 @@ body[data-ds-dark-theme]{
4883
4883
  onClick: onBack
4884
4884
  }, `← ${t("backToConversation")}`), (0, react.createElement)("div", { style: titleStyle }, t("panelTitle"), (0, react.createElement)("span", {
4885
4885
  style: buildTagStyle,
4886
- title: `v0.0.97 · 261027-114255-561`
4887
- }, `v0.0.97 261027-114255-561`))), (0, react.createElement)("div", { style: {
4886
+ title: `v0.0.98 · 261027-120937-454`
4887
+ }, `v0.0.98 261027-120937-454`))), (0, react.createElement)("div", { style: {
4888
4888
  display: "flex",
4889
4889
  alignItems: "center",
4890
4890
  gap: "var(--dtl-space-2)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-task-ledger",
3
- "version": "0.0.97",
3
+ "version": "0.0.98",
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.23
4
+ version: 1.3.24
5
5
  allowed-tools: Read,Edit,Bash,Grep
6
6
  ---
7
7
 
@@ -207,7 +207,16 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
207
207
 
208
208
  - 「执行记录」节名一个字都别动;老文档缺这一节也不用补。
209
209
  - 过程文档找不到时 `log` 只提示不失败;用 `show` 查路径,不要手写。
210
- - 头部由脚本维护:创建时间 / 创建人、状态行(`> **状态**:doing · @某人(uuid)· 更新 <时间>`)、封卷标记、分类 / 标签行 —— **都不要手改**。老文档没有状态行是合法的(`show` 回退查在办,`--json` 标 `legacy: true`),不用补。
210
+ - 头部是四行,由脚本维护、**都不要手改**,顺序固定 `分类/标签 → 状态 → 创建 → 封卷`,值一律 `· ` 分隔:
211
+
212
+ ```markdown
213
+ - **分类**:建设 · **标签**:DSH、台账
214
+ - **状态**:doing · 2026-10-27 09:12:33 · @某人 · 8d557c6d-…
215
+ - **创建**:2026-10-26 23:38:02 · @某人 · 8d557c6d-…
216
+ - **封卷**:2026-10-27 09:20:10 · @某人 · 8d557c6d-…
217
+ ```
218
+
219
+ 分类 / 标签行与封卷行不一定有(没给分类、没结案就没有)。老文档没有状态行是合法的(`show` 回退查在办,`--json` 标 `legacy: true`),不用补。
211
220
  - 执行记录行格式 `- 时间(UTC) · 类型 · @谁 · 说明`,`@谁` 是这次动手的人、不是任务现归属;只追加、不改旧行。行首「类型」是脚本内部动作名(会见到 `release`,它就是 `set --release`);认不出就别当成新命令,按 `--help` 查。
212
221
  - 只改某条任务的正文:`show --id <任务号>` 拿路径直接改,不另建 md;已结案的用 `history --months 12 --json` 找回链接(**人话输出里没有链接,必须加 `--json`**)——默认只查近 3 个月,结案久了的要显式放大窗口;只读不改。
213
222
 
@@ -1,9 +1,11 @@
1
1
  # <id>:<title>
2
2
 
3
- > **创建**:<created>(@<creator>)
4
- > **状态**:<status>
3
+ - **状态**:<status>
4
+ - **创建**:<created> · @<creator>
5
5
 
6
- <!-- 「> **分类**:… | **标签**:…」行由脚本按 new --category / --tags 插入;两者皆空则不写。 -->
6
+ <!-- 头部四行由脚本维护,顺序固定:分类/标签 → 状态 → 创建 → 封卷。
7
+ 「- **分类**:… · **标签**:…」按 new --category / --tags 插在最上面,两者皆空则不写;
8
+ 「- **封卷**:…」在结案时加在最后。 -->
7
9
 
8
10
  ## 背景
9
11
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-ledger",
3
- "version": "1.3.23",
3
+ "version": "1.3.24",
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",
@@ -34,6 +34,7 @@ def show_task(repo: Path, me: Identity, tid: str) -> ShowResult:
34
34
  "owner_name": status.name,
35
35
  "created": worklog_md.head_created(text),
36
36
  "updated": status.when,
37
+ "finished": worklog_md.head_frozen(text),
37
38
  },
38
39
  worklog=WorklogView(path=rel(repo, path), text=text, error=None),
39
40
  )
@@ -27,10 +27,10 @@ class Identity:
27
27
  def stamp(self) -> str:
28
28
  """**落盘**用的身份串:`名字 · uuid` —— 名字在前给人读,uuid 在后给机器对。
29
29
 
30
- 与 `label()` 一样**不带 `@`**(`@` 由各落点自己加:模板创建行是 `(@<creator>)`、
31
- `exec_line` 拼 ` · @{who} · `、`stamp_frozen` 拼 `(@{who})`)。
30
+ 与 `label()` 一样**不带 `@`**(`@` 由各落点自己加:头部各行拼 ` · @{stamp}`、
31
+ `exec_line` 拼 ` · @{who} · `)。
32
32
 
33
- 过程文档(创建行 / 封卷行 / 执行记录)是这个串唯一的落点:文件是最终产物,
33
+ 过程文档(创建行 / 状态行 / 封卷行 / 执行记录)是这个串唯一的落点:文件是最终产物,
34
34
  创建人和封卷人可能不是同一个人、也可能中途改过名,所以**两个都要留**,不靠
35
35
  历史表回链。没名字就只写 uuid。
36
36
  """
@@ -15,7 +15,7 @@ LABEL_MAX_WORDS_EN = 3 # 纯 ASCII 时按「词」算
15
15
  TAGS_MAX_COUNT = 5
16
16
 
17
17
  CJK_RE = re.compile(r"[\u4e00-\u9fff]")
18
- LABEL_CAT_RE = re.compile(r"分类\*\*\s*[::]\s*([^||]+)")
18
+ LABEL_CAT_RE = re.compile(r"分类\*\*\s*[::]\s*([^||·]+)")
19
19
  LABEL_TAG_RE = re.compile(r"标签\*\*\s*[::]\s*(.+)$")
20
20
  _SPLIT_RE = re.compile(r"[,,、]")
21
21
 
@@ -49,21 +49,33 @@ def parse_tags(raw: str | Iterable[str] | None) -> tuple[str, ...]:
49
49
 
50
50
 
51
51
  def label_line(category: str | None, tags: Iterable[str] | None) -> str:
52
- """过程文档头部的「分类/标签」行;两者皆空返回空串(此行不写)。"""
52
+ """过程文档头部的「分类 / 标签」行;两者皆空返回空串(此行不写)。"""
53
53
  segs: list[str] = []
54
54
  if category:
55
55
  segs.append(f"**分类**:{category}")
56
56
  tag_list = [t for t in (tags or ()) if t]
57
57
  if tag_list:
58
58
  segs.append(f"**标签**:{'、'.join(tag_list)}")
59
- return "> " + " | ".join(segs) if segs else ""
59
+ return "- " + " · ".join(segs) if segs else ""
60
60
 
61
61
 
62
62
  def parse_label_line(text: str) -> tuple[str | None, tuple[str, ...]]:
63
- """从过程文档头部(第一个 `## ` 小节之前)解析分类与标签。"""
63
+ """从过程文档头部(第一个 `## ` 小节之前)解析分类与标签。
64
+
65
+ 跳过 HTML 注释块:模板注释里举例写着「**分类**:…」,那不是数据。
66
+ """
67
+ in_comment = False
64
68
  for line in text.splitlines():
65
69
  if line.startswith("## "):
66
70
  break
71
+ stripped = line.lstrip()
72
+ if in_comment:
73
+ if "-->" in stripped:
74
+ in_comment = False
75
+ continue
76
+ if stripped.startswith("<!--"):
77
+ in_comment = "-->" not in stripped
78
+ continue
67
79
  mc = LABEL_CAT_RE.search(line)
68
80
  mt = LABEL_TAG_RE.search(line)
69
81
  if not (mc or mt):
@@ -20,33 +20,44 @@ EXEC_HINT = (
20
20
  "<!-- 本节由脚本维护:new / claim / set / release / log / close 自动追加,请勿手改。\n"
21
21
  ' 补记用 progress.py log --id <任务号> --note "一句话"。 -->'
22
22
  )
23
- FROZEN_MARK = "✅ 完成封卷"
24
- FROZEN_RE = re.compile(r"^>\s*" + FROZEN_MARK, re.MULTILINE)
25
- CREATED_RE = re.compile(r"^\s*>\s*\*\*创建\*\*")
26
- # 头部「创建」行:`> **创建**:2026-10-06 00:00(@某人 · uuid)` —— 创建时间与发起人两个来源。
27
- # 括号里落 `Identity.stamp()`(`@名字 · uuid`):名字在前给人读,uuid 在后给机器对。
28
- # 老文档只有名字(没有 uuid)时,下面两个函数各自返回它能读到的那一段,不报错。
23
+ FROZEN_MARK = "封卷"
24
+ #: 头部四行的规范顺序:分类/标签 → 状态 → 创建 → 封卷。
25
+ #: 「分类」只在有分类 / 标签时写,「封卷」只在结案时写,另两行必有。
26
+ HEAD_KEY_ORDER = ("meta", "status", "created", "frozen")
27
+ #: 头部行前缀:新格式是无序列表 `- `,老文档是引用块 `> ` —— 读的时候两种都认。
28
+ _HEAD_BULLET = r"^(?:\s*>\s?|\s*[-*]\s+)"
29
+ FROZEN_RE = re.compile(
30
+ _HEAD_BULLET + r"(?:\*\*" + FROZEN_MARK + r"\*\*|✅ 完成封卷)", re.MULTILINE
31
+ )
32
+ #: 封卷行的时间(老文档是 `> ✅ 完成封卷:<时间>(@…)`,新格式是 `- **封卷**:<时间> · @…`)。
33
+ _FROZEN_AT_RE = re.compile(
34
+ _HEAD_BULLET + r"(?:\*\*" + FROZEN_MARK + r"\*\*|✅ 完成封卷)[^::]*[::]\s*"
35
+ r"(\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}:\d{2}(?::\d{2})?)?)"
36
+ )
37
+ # 头部「创建」行:`- **创建**:2026-10-06 00:00 · @某人 · uuid`。
38
+ # 值一律 `时间 · @名字 · uuid`,与执行记录行同一种分隔符;老文档是 `(@名字 · uuid)`,读得到就认。
39
+ CREATED_RE = re.compile(_HEAD_BULLET + r"\*\*创建\*\*")
29
40
  CREATED_AT_RE = re.compile(
30
- r"^\s*>\s*\*\*创建\*\*\s*[::]\s*"
41
+ _HEAD_BULLET + r"\*\*创建\*\*\s*[::]\s*"
31
42
  r"(\d{4}-\d{2}-\d{2}(?: +\d{1,2}:\d{1,2}(?::\d{1,2})?)?)"
32
43
  )
33
- CREATOR_RE = re.compile(r"^\s*>\s*\*\*创建\*\*\s*[::].*?(@(.+?))")
44
+ CREATOR_RE = re.compile(_HEAD_BULLET + r"\*\*创建\*\*\s*[::].*?@(.+?)\s*[))]?\s*$")
34
45
 
35
- # 头部「状态」行:`> **状态**:doing · @某人(uuid)· 更新 2026-10-27 08:28:22`。
46
+ # 头部「状态」行:`- **状态**:doing · 2026-10-27 08:28:22 · @某人 · uuid`。
36
47
  # 它是**镜像**:分片 / 池里的 `status` / `owner` 才是权威,这行只是让「看一条详情」
37
- # 不必再翻索引(2026-10-27 用户拍板)。老文档没有这行 ⇒ 合法,读取方回退查分片。
48
+ # 不必再翻索引。老文档没有这行 ⇒ 合法,读取方回退查分片。
38
49
  STATUS_MARK = "状态"
39
- STATUS_LINE_RE = re.compile(r"^\s*>\s*\*\*" + STATUS_MARK + r"\*\*")
40
- STATUS_RE = re.compile(r"^\s*>\s*\*\*" + STATUS_MARK + r"\*\*\s*[::]\s*([A-Za-z_]+)")
50
+ STATUS_LINE_RE = re.compile(_HEAD_BULLET + r"\*\*" + STATUS_MARK + r"\*\*")
51
+ STATUS_RE = re.compile(
52
+ _HEAD_BULLET + r"\*\*" + STATUS_MARK + r"\*\*\s*[::]\s*([A-Za-z_]+)"
53
+ )
41
54
  #: 状态取值(与 `core/labels.STATUSES` + 结案后的 `closed` 同口径,全链路英文)。
42
55
  STATUS_VALUES = ("open", "doing", "closed")
43
56
  _STATUS_UUID_RE = re.compile(
44
57
  r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
45
58
  )
46
59
  _STATUS_NAME_RE = re.compile(r"@([^((·]+)")
47
- _STATUS_WHEN_RE = re.compile(
48
- r"更新\s+(\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}:\d{2}(?::\d{2})?)?)"
49
- )
60
+ _STATUS_WHEN_RE = re.compile(r"(\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}:\d{2}(?::\d{2})?)?)")
50
61
  _TITLE_RE = re.compile(r"^#\s*(.+?)\s*[::]\s*(.*)$")
51
62
 
52
63
  _HEAD_RE = re.compile(r"\A(.*?)(?=^## |\Z)", re.DOTALL | re.MULTILINE)
@@ -84,10 +95,10 @@ def format_problems(text: str) -> list[str]:
84
95
  head = _head_lines(lines)
85
96
  if not any(CREATED_AT_RE.match(line) for line in head):
86
97
  problems.append(
87
- "头部缺「创建」时间行(应为 `> **创建**:YYYY-MM-DD HH:MM(@某人)`)"
98
+ "头部缺「创建」时间行(应为 `- **创建**:YYYY-MM-DD HH:MM:SS · @某人 · uuid`)"
88
99
  )
89
100
  elif not any(CREATOR_RE.match(line) for line in head):
90
- problems.append("头部「创建」行缺发起人(应为 `…(@某人)`)")
101
+ problems.append("头部「创建」行缺发起人(应为 `… · @某人 · uuid`)")
91
102
  # 状态行:**有就必须合法**;缺了不报——老文档没有这行是合法的(读取方回退查分片)
92
103
  for line in head:
93
104
  if not STATUS_LINE_RE.match(line):
@@ -164,8 +175,8 @@ def render_worklog(
164
175
  ) -> str:
165
176
  """把模板套成一篇过程文档(`<id>` / `<title>` / `<created>` / `<creator>` / `<status>`)。
166
177
 
167
- `<status>` 只占状态行的**内容**(`doing · @某人(uuid)· 更新 …`);模板里的
168
- `> **状态**:` 前缀是字面量。不传 status ⇒ 整行删掉(不留空壳)。
178
+ `<status>` 只占状态行的**内容**(`doing · 时间 · @某人 · uuid`);模板里的
179
+ `- **状态**:` 前缀是字面量。不传 status ⇒ 整行删掉(不留空壳)。
169
180
  """
170
181
  text = (
171
182
  template.replace("<id>", tid)
@@ -190,15 +201,50 @@ def render_worklog(
190
201
  meta = label_line(category, tags)
191
202
  if not meta:
192
203
  return text
193
- return "\n".join(_insert_after_head(text.splitlines(), meta)) + "\n"
204
+ return "\n".join(_insert_head_line(text.splitlines(), meta, "meta")) + "\n"
194
205
 
195
206
 
196
- def _insert_after_head(lines: list[str], line: str) -> list[str]:
197
- """把一行插到「创建」行之后(头部引用块内);找不到创建行就插标题后。"""
198
- at = next((i for i, ln in enumerate(lines) if CREATED_RE.match(ln)), None)
199
- if at is None:
207
+ _META_LINE_RE = re.compile(_HEAD_BULLET + r"\*\*(?:分类|标签)\*\*")
208
+
209
+
210
+ def _head_key(line: str) -> str | None:
211
+ """这一行是头部哪个字段(`meta` / `status` / `created` / `frozen`);都不是给 None。"""
212
+ if _META_LINE_RE.search(line):
213
+ return "meta"
214
+ if STATUS_LINE_RE.match(line):
215
+ return "status"
216
+ if CREATED_RE.match(line):
217
+ return "created"
218
+ if FROZEN_RE.match(line):
219
+ return "frozen"
220
+ return None
221
+
222
+
223
+ def _insert_head_line(lines: list[str], line: str, key: str) -> list[str]:
224
+ """按 `HEAD_KEY_ORDER` 把一行插进头部(第一个 `## ` 之前)。
225
+
226
+ 排在它后面的字段行先出现就插在那行前面;没有就接在最后一个字段行之后;
227
+ 一个字段行都没有就插在标题(及其后的空行)下面。
228
+ """
229
+ rank = HEAD_KEY_ORDER.index(key)
230
+ last: int | None = None
231
+ for i, ln in enumerate(lines):
232
+ if ln.startswith("## "):
233
+ break
234
+ found = _head_key(ln)
235
+ if found is None:
236
+ continue
237
+ if HEAD_KEY_ORDER.index(found) > rank:
238
+ lines.insert(i, line)
239
+ return lines
240
+ last = i
241
+ if last is None:
200
242
  at = 1 if lines and lines[0].startswith("# ") else 0
201
- lines.insert(at + 1, line)
243
+ while at + 1 < len(lines) and not lines[at + 1].strip():
244
+ at += 1
245
+ lines.insert(at + 1, line)
246
+ else:
247
+ lines.insert(last + 1, line)
202
248
  return lines
203
249
 
204
250
 
@@ -217,16 +263,14 @@ def render_status_body(
217
263
  uuid: str | None = None,
218
264
  when: str | None = None,
219
265
  ) -> str:
220
- """状态行的**内容**部分(`> **状态**:` 之后那段)。"""
266
+ """状态行的**内容**部分(`- **状态**:` 之后那段):`状态 · 时间 · @名字 · uuid`。"""
221
267
  parts = [status]
222
- if name and uuid:
223
- parts.append(f"@{name}({uuid})")
224
- elif name:
268
+ if when:
269
+ parts.append(when)
270
+ if name:
225
271
  parts.append(f"@{name}")
226
- elif uuid:
272
+ if uuid:
227
273
  parts.append(uuid)
228
- if when:
229
- parts.append(f"更新 {when}")
230
274
  return " · ".join(parts)
231
275
 
232
276
 
@@ -236,11 +280,11 @@ def render_status_line(
236
280
  uuid: str | None = None,
237
281
  when: str | None = None,
238
282
  ) -> str:
239
- return f"> **{STATUS_MARK}**:{render_status_body(status, name, uuid, when)}"
283
+ return f"- **{STATUS_MARK}**:{render_status_body(status, name, uuid, when)}"
240
284
 
241
285
 
242
286
  def rewrite_status_line(text: str, status: StatusLine) -> str:
243
- """把头部状态行改成 `status`:有就就地替换,没有就插到「创建」行后。"""
287
+ """把头部状态行改成 `status`:有就就地替换,没有就按头部顺序插进去。"""
244
288
  line = render_status_line(status.status, status.name, status.uuid, status.when)
245
289
  lines = (text or "").splitlines()
246
290
  for i, ln in enumerate(lines):
@@ -249,7 +293,7 @@ def rewrite_status_line(text: str, status: StatusLine) -> str:
249
293
  if STATUS_LINE_RE.match(ln):
250
294
  lines[i] = line
251
295
  return "\n".join(lines) + "\n"
252
- return "\n".join(_insert_after_head(lines, line)) + "\n"
296
+ return "\n".join(_insert_head_line(lines, line, "status")) + "\n"
253
297
 
254
298
 
255
299
  def read_status(text: str) -> StatusLine | None:
@@ -309,25 +353,25 @@ def has_frozen_line(text: str) -> bool:
309
353
 
310
354
 
311
355
  def stamp_frozen(text: str, finished: str, who: str | None = "") -> str:
312
- """封卷:在头部打标记行(`finished` 传 UTC,原样写盘——见 exec_line 的口径说明)。"""
313
- mark = f"> {FROZEN_MARK}:{finished}"
356
+ """封卷:在头部打「封卷」行(`finished` 传 UTC,原样写盘——见 exec_line 的口径说明)。"""
357
+ mark = f"- **{FROZEN_MARK}**:{finished}"
314
358
  if who:
315
- mark += f"(@{who})"
359
+ mark += f" · @{who}"
316
360
  if FROZEN_RE.search(text or ""):
317
361
  return text
318
- return "\n".join(_insert_after_head(text.splitlines(), mark)) + "\n"
362
+ return "\n".join(_insert_head_line(text.splitlines(), mark, "frozen")) + "\n"
319
363
 
320
364
 
321
365
  def rewrite_label_line(
322
366
  text: str, category: str | None, tags: Iterable[str] | None
323
367
  ) -> str:
324
- """改写头部的分类 / 标签行:有就地替换,没有就插到创建行后。"""
368
+ """改写头部的分类 / 标签行:有就地替换,没有就按头部顺序插进去。"""
325
369
  line = label_line(category, tags)
326
370
  lines = text.splitlines()
327
371
  for i, ln in enumerate(lines):
328
372
  if ln.startswith("## "):
329
373
  break
330
- if "**分类**" in ln or "**标签**" in ln:
374
+ if _META_LINE_RE.match(ln):
331
375
  if line:
332
376
  lines[i] = line
333
377
  else:
@@ -335,7 +379,7 @@ def rewrite_label_line(
335
379
  return "\n".join(lines) + "\n"
336
380
  if not line:
337
381
  return text
338
- return "\n".join(_insert_after_head(lines, line)) + "\n"
382
+ return "\n".join(_insert_head_line(lines, line, "meta")) + "\n"
339
383
 
340
384
 
341
385
  def head_meta(text: str) -> tuple[str | None, tuple[str, ...]]:
@@ -355,8 +399,20 @@ def head_created(text: str) -> str | None:
355
399
  return None
356
400
 
357
401
 
402
+ def head_frozen(text: str) -> str | None:
403
+ """头部「封卷」行的时间(原样返回,不转时区);没封卷 / 读不到返回 None。"""
404
+ for line in text.splitlines():
405
+ m = _FROZEN_AT_RE.match(line)
406
+ if m:
407
+ try:
408
+ return normalize_time(m.group(1), field="封卷")
409
+ except Fail:
410
+ return None
411
+ return None
412
+
413
+
358
414
  def head_creator(text: str) -> str | None:
359
- """头部「创建」行括号里的发起人**名字**(不带 @、不带 uuid);读不到返回 None。"""
415
+ """头部「创建」行的发起人**名字**(不带 @、不带 uuid);读不到返回 None。"""
360
416
  for line in text.splitlines():
361
417
  m = CREATOR_RE.match(line)
362
418
  if m and m.group(1).strip():
@@ -365,7 +421,7 @@ def head_creator(text: str) -> str | None:
365
421
 
366
422
 
367
423
  def head_creator_uuid(text: str) -> str | None:
368
- """头部「创建」行括号里的发起人 **uuid**;老文档没落 uuid 时返回 None。
424
+ """头部「创建」行的发起人 **uuid**;老文档没落 uuid 时返回 None。
369
425
 
370
426
  `UUID_RE` 带 `^$` 锚点,只能整串匹配 —— 所以先按 ` · ` 切出尾段再判,不能 search。
371
427
  """
@@ -23,7 +23,8 @@ from .atomic import ensure_dir, write_text_atomic
23
23
 
24
24
  FALLBACK_TEMPLATE = """# <id>:<title>
25
25
 
26
- > **创建**:<created>(@<creator>)
26
+ - **状态**:<status>
27
+ - **创建**:<created> · @<creator>
27
28
 
28
29
  ## 背景
29
30