dsh-task-ledger 0.0.96 → 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.
Files changed (37) hide show
  1. package/dist/client.js +3618 -116
  2. package/dist/client.js.map +1 -1
  3. package/dist/host/repositories/ledger.repository.d.ts +8 -2
  4. package/dist/host/repositories/ledger.repository.js +4 -1
  5. package/dist/host/repositories/records.d.ts +10 -2
  6. package/dist/host/repositories/records.js +11 -2
  7. package/dist/host/routes.js +1 -0
  8. package/dist/host/services/dates.d.ts +7 -2
  9. package/dist/host/services/dates.js +29 -6
  10. package/dist/host/services/history.service.js +1 -1
  11. package/dist/host/services/trend.service.d.ts +4 -1
  12. package/dist/host/services/trend.service.js +9 -1
  13. package/package.json +4 -1
  14. package/skill/README.md +2 -2
  15. package/skill/SKILL.md +43 -13
  16. package/skill/assets/worklog.md +5 -2
  17. package/skill/package.json +1 -1
  18. package/skill/references/state-model.md +6 -1
  19. package/skill/references/troubleshooting.md +5 -5
  20. package/skill/scripts/ledgerkit/actions/claim.py +7 -2
  21. package/skill/scripts/ledgerkit/actions/close_task.py +8 -0
  22. package/skill/scripts/ledgerkit/actions/handoff.py +5 -2
  23. package/skill/scripts/ledgerkit/actions/list_tasks.py +4 -1
  24. package/skill/scripts/ledgerkit/actions/log_progress.py +6 -2
  25. package/skill/scripts/ledgerkit/actions/new_task.py +7 -1
  26. package/skill/scripts/ledgerkit/actions/query_history.py +3 -1
  27. package/skill/scripts/ledgerkit/actions/release.py +6 -2
  28. package/skill/scripts/ledgerkit/actions/result.py +2 -0
  29. package/skill/scripts/ledgerkit/actions/show_task.py +32 -4
  30. package/skill/scripts/ledgerkit/actions/validate.py +6 -49
  31. package/skill/scripts/ledgerkit/core/identity.py +3 -3
  32. package/skill/scripts/ledgerkit/core/labels.py +16 -4
  33. package/skill/scripts/ledgerkit/core/time.py +2 -1
  34. package/skill/scripts/ledgerkit/core/worklog_md.py +216 -31
  35. package/skill/scripts/ledgerkit/shell/cli.py +24 -2
  36. package/skill/scripts/ledgerkit/shell/render.py +48 -4
  37. package/skill/scripts/ledgerkit/store/worklog_store.py +17 -5
@@ -19,8 +19,14 @@ export declare function hasLedger(workspacePath: string): boolean;
19
19
  * 不静默返空(空 = 撒谎)。**过 1.5s 读缓存**(同屏多端点会反复读同一份在办)。
20
20
  */
21
21
  export declare function readTasks(workspace: LedgerWorkspace, filter?: LedgerFilter): Promise<TaskRow[]>;
22
- /** 已结案记录(包 `history --json`;`records` 必须存在且为数组,缺了即坏数据)。同样过读缓存。 */
23
- export declare function readHistory(workspace: LedgerWorkspace, filter?: LedgerFilter): Promise<HistoryRecord[]>;
22
+ /**
23
+ * 已结案记录(包 `history --json`;`records` 必须存在且为数组,缺了即坏数据)。同样过读缓存。
24
+ * 一并带回 `warnings`(被脚本整行跳过的坏行原因)——调用方必须展示,不许吞。
25
+ */
26
+ export declare function readHistory(workspace: LedgerWorkspace, filter?: LedgerFilter): Promise<{
27
+ records: HistoryRecord[];
28
+ warnings: string[];
29
+ }>;
24
30
  /** 单条在办(展开行 / 详情用)。取不到回 null,不抛。 */
25
31
  export declare function getTask(workspace: LedgerWorkspace, taskId: string): Promise<TaskRow | null>;
26
32
  /**
@@ -65,7 +65,10 @@ export async function readTasks(workspace, filter = {}) {
65
65
  return parseTaskList(result.data, workspace);
66
66
  });
67
67
  }
68
- /** 已结案记录(包 `history --json`;`records` 必须存在且为数组,缺了即坏数据)。同样过读缓存。 */
68
+ /**
69
+ * 已结案记录(包 `history --json`;`records` 必须存在且为数组,缺了即坏数据)。同样过读缓存。
70
+ * 一并带回 `warnings`(被脚本整行跳过的坏行原因)——调用方必须展示,不许吞。
71
+ */
69
72
  export async function readHistory(workspace, filter = {}) {
70
73
  return await cached(cacheKey(workspace.id, "history", argsKey(filter, "history")), async () => {
71
74
  const result = await runLedgerScript("history", [
@@ -1,5 +1,13 @@
1
1
  import type { HistoryRecord, LedgerWorkspace, TaskRow } from "../types.js";
2
2
  /** `list --json`(任务号为键的对象)→ TaskRow[](打上 id 与 workspace)。 */
3
3
  export declare function parseTaskList(raw: unknown, workspace: LedgerWorkspace): TaskRow[];
4
- /** `history --json`(`{count, filters, records}`)→ HistoryRecord[](打上 workspace)。 */
5
- export declare function parseHistoryRecords(raw: unknown, workspace: LedgerWorkspace): HistoryRecord[];
4
+ /**
5
+ * `history --json`(`{count, filters, records, warnings}`)→ 记录 + 脚本警告(都打上 workspace)。
6
+ *
7
+ * ⚠️ `warnings` **必须带出去**:脚本只认十一列历史,列数不对 / 时间不合法的行会被
8
+ * **整行跳过**(不是少几个字段)。丢掉 warnings 就是「趋势图少算一截还装作没发生」。
9
+ */
10
+ export declare function parseHistoryRecords(raw: unknown, workspace: LedgerWorkspace): {
11
+ records: HistoryRecord[];
12
+ warnings: string[];
13
+ };
@@ -29,7 +29,12 @@ export function parseTaskList(raw, workspace) {
29
29
  }
30
30
  return rows;
31
31
  }
32
- /** `history --json`(`{count, filters, records}`)→ HistoryRecord[](打上 workspace)。 */
32
+ /**
33
+ * `history --json`(`{count, filters, records, warnings}`)→ 记录 + 脚本警告(都打上 workspace)。
34
+ *
35
+ * ⚠️ `warnings` **必须带出去**:脚本只认十一列历史,列数不对 / 时间不合法的行会被
36
+ * **整行跳过**(不是少几个字段)。丢掉 warnings 就是「趋势图少算一截还装作没发生」。
37
+ */
33
38
  export function parseHistoryRecords(raw, workspace) {
34
39
  if (!isPlainObject(raw)) {
35
40
  throw new LedgerDataError(workspace.id, `${workspace.title}: 台账 history 输出不是对象,拒绝当成空历史`);
@@ -44,7 +49,11 @@ export function parseHistoryRecords(raw, workspace) {
44
49
  continue; // 元素级宽松:坏行跳过,不整板炸
45
50
  rows.push(toHistoryRecord(record, workspace));
46
51
  }
47
- return rows;
52
+ const rawWarnings = raw.warnings;
53
+ const warnings = Array.isArray(rawWarnings)
54
+ ? rawWarnings.filter((item) => typeof item === "string")
55
+ : [];
56
+ return { records: rows, warnings };
48
57
  }
49
58
  /**
50
59
  * 脚本 9 列的键 → 领域字段名。
@@ -135,6 +135,7 @@ export function registerRoutes(webServer, deps) {
135
135
  closed: result.closed,
136
136
  trend: result.trend,
137
137
  degraded: result.degraded,
138
+ warnings: result.warnings,
138
139
  });
139
140
  }
140
141
  catch (error) {
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * 日期口径(纯函数层,零 IO):本机时区、周一为一周之始(与 skill 脚本同口径)。
3
3
  * 所有函数显式收 `now`——不自己读时钟,便于单测与跨时区复现。
4
+ *
5
+ * ⚠️ **台账存盘时间是 UTC**(skill `core/time.py::now`),`--json` 原样吐 UTC,
6
+ * 只有人话输出才转查看者本地(`fmt_local`)。看板是给人看的 ⇒ 一律先按 UTC 解析出
7
+ * 真实时刻,再按本机时区取日期(等价于脚本的 `fmt_local`)。否则会出现
8
+ * 「脚本表格里是 10-17、看板却算进 10-16 那周」—— 两个界面同一条记录差一天。
4
9
  */
5
- /** Date → `YYYY-MM-DD`。 */
10
+ /** Date → `YYYY-MM-DD`(本机时区)。 */
6
11
  export declare function ymd(date: Date): string;
7
- /** 时刻串(`YYYY-MM-DD HH:mm:ss` / `YYYY-MM-DD`)→ 日期部分;解析不了回 null。 */
12
+ /** 时刻串(`YYYY-MM-DD HH:mm:ss` / `YYYY-MM-DD`)→ 本机时区的日期;解析不了回 null。 */
8
13
  export declare function dayOf(stamp: string | null | undefined): string | null;
9
14
  /** 某日期所在周的周一(00:00)。 */
10
15
  export declare function mondayOf(day: string): Date;
@@ -1,20 +1,43 @@
1
1
  /**
2
2
  * 日期口径(纯函数层,零 IO):本机时区、周一为一周之始(与 skill 脚本同口径)。
3
3
  * 所有函数显式收 `now`——不自己读时钟,便于单测与跨时区复现。
4
+ *
5
+ * ⚠️ **台账存盘时间是 UTC**(skill `core/time.py::now`),`--json` 原样吐 UTC,
6
+ * 只有人话输出才转查看者本地(`fmt_local`)。看板是给人看的 ⇒ 一律先按 UTC 解析出
7
+ * 真实时刻,再按本机时区取日期(等价于脚本的 `fmt_local`)。否则会出现
8
+ * 「脚本表格里是 10-17、看板却算进 10-16 那周」—— 两个界面同一条记录差一天。
4
9
  */
5
10
  function pad(value) {
6
11
  return String(value).padStart(2, "0");
7
12
  }
8
- /** Date → `YYYY-MM-DD`。 */
13
+ /** Date → `YYYY-MM-DD`(本机时区)。 */
9
14
  export function ymd(date) {
10
15
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
11
16
  }
12
- /** 时刻串(`YYYY-MM-DD HH:mm:ss` / `YYYY-MM-DD`)→ 日期部分;解析不了回 null。 */
17
+ /**
18
+ * 台账时刻串 → Date(带时刻的按 **UTC** 解析;纯日期串按本地日历,不涉及时区)。
19
+ * 解析不了回 null,不猜。
20
+ */
21
+ function parseStamp(stamp) {
22
+ const text = stamp.trim().replace(" ", "T");
23
+ // 已带时区后缀(`Z` / `+08:00`)的不重复加,加了会二次偏移
24
+ const iso = /\d{1,2}:\d{2}/.test(text)
25
+ ? /(?:[zZ]|[+-]\d{2}:?\d{2})$/.test(text)
26
+ ? text
27
+ : `${text}Z`
28
+ : text;
29
+ const parsed = new Date(iso);
30
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
31
+ }
32
+ /** 时刻串(`YYYY-MM-DD HH:mm:ss` / `YYYY-MM-DD`)→ 本机时区的日期;解析不了回 null。 */
13
33
  export function dayOf(stamp) {
14
34
  if (stamp === null || stamp === undefined || stamp === "")
15
35
  return null;
16
- const day = String(stamp).slice(0, 10);
17
- return /^\d{4}-\d{2}-\d{2}$/.test(day) ? day : null;
36
+ const text = String(stamp);
37
+ if (/^\d{4}-\d{2}-\d{2}$/.test(text.trim()))
38
+ return text.trim();
39
+ const parsed = parseStamp(text);
40
+ return parsed === null ? null : ymd(parsed);
18
41
  }
19
42
  /** 某日期所在周的周一(00:00)。 */
20
43
  export function mondayOf(day) {
@@ -51,8 +74,8 @@ export function inWindow(day, window) {
51
74
  export function daysSince(stamp, now) {
52
75
  if (stamp === null || stamp === undefined || stamp === "")
53
76
  return null;
54
- const parsed = new Date(String(stamp).replace(" ", "T"));
55
- if (Number.isNaN(parsed.getTime()))
77
+ const parsed = parseStamp(String(stamp));
78
+ if (parsed === null)
56
79
  return null;
57
80
  const days = Math.floor((now.getTime() - parsed.getTime()) / 86_400_000);
58
81
  return days > 0 ? days : 0;
@@ -15,7 +15,7 @@ export const DEFAULT_HISTORY_LIMIT = 500;
15
15
  export async function queryClosed(workspaces, filter = {}) {
16
16
  const windowed = { months: RECENT_MONTHS, ...filter };
17
17
  const { data, degraded } = await settleWorkspaces(workspaces, "history", (workspace) => readHistory(workspace, windowed), (rows) => rows
18
- .flat()
18
+ .flatMap((row) => row.records)
19
19
  .sort((a, b) => String(b.created ?? b.time ?? "").localeCompare(String(a.created ?? a.time ?? "")))
20
20
  .slice(0, filter.limit ?? DEFAULT_HISTORY_LIMIT));
21
21
  return { records: data, degraded };
@@ -12,7 +12,9 @@ export declare const RECENT_MONTHS = 13;
12
12
  /** 趋势图桶数:近 8 周(2026-10-17 定稿)。 */
13
13
  export declare const WEEKLY_WEEKS = 8;
14
14
  /** 慢路径读法:单工作区 readTasks + readHistory(months) + 本机身份(并行;供 settle 逐工作区调)。 */
15
- export declare function readTrendScope(workspace: LedgerWorkspace): Promise<TrendScope>;
15
+ export declare function readTrendScope(workspace: LedgerWorkspace): Promise<TrendScope & {
16
+ warnings: readonly string[];
17
+ }>;
16
18
  /**
17
19
  * 近 N 周分组桶(完成按结案时间、新增按创建时间)× 所有 / 我的(纯函数)。
18
20
  *
@@ -28,6 +30,7 @@ export declare function getTrend(workspaces: readonly LedgerWorkspace[]): Promis
28
30
  closed: ClosedSummary;
29
31
  trend: TrendPeriods;
30
32
  degraded: readonly DegradedInfo[];
33
+ warnings: readonly string[];
31
34
  }>;
32
35
  /** 趋势端点用的历史 filter(窗口由本模块策略决定,repository 零默认值注入)。 */
33
36
  export declare function trendHistoryFilter(extra?: LedgerFilter): LedgerFilter;
@@ -21,7 +21,13 @@ export async function readTrendScope(workspace) {
21
21
  readHistory(workspace, { months: RECENT_MONTHS }),
22
22
  getCurrentIdentity(workspace),
23
23
  ]);
24
- return { workspace, tasks, history, identity };
24
+ return {
25
+ workspace,
26
+ tasks,
27
+ history: history.records,
28
+ identity,
29
+ warnings: history.warnings,
30
+ };
25
31
  }
26
32
  /**
27
33
  * 近 N 周分组桶(完成按结案时间、新增按创建时间)× 所有 / 我的(纯函数)。
@@ -146,6 +152,8 @@ export async function getTrend(workspaces) {
146
152
  closed: computeClosedSummary(scopes, now),
147
153
  trend: computeTrendPeriods(scopes, now),
148
154
  degraded,
155
+ // 被脚本跳过的历史行(列数不对 / 时间不合法):数字已经少算了,必须让界面说出口
156
+ warnings: scopes.flatMap((scope) => scope.warnings),
149
157
  };
150
158
  }
151
159
  /** 趋势端点用的历史 filter(窗口由本模块策略决定,repository 零默认值注入)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-task-ledger",
3
- "version": "0.0.96",
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",
@@ -55,5 +55,8 @@
55
55
  "@deepseek-ai/dsh-client-ui-layout"
56
56
  ]
57
57
  }
58
+ },
59
+ "dependencies": {
60
+ "dsh-plugin-core": "^0.5.0"
58
61
  }
59
62
  }
package/skill/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  ## 为什么这么设计
26
26
 
27
- 跨项目固定用 `ledger/`(不占项目的 `docs/`);状态用 JSON 而非 Markdown(改动面最小);状态与过程正文分离(互不踩);一人一份分片把并发冲突面从整份文件缩到公共池里几行;台账数据时间存 UTC、过程文档里的日志行显示本地时间。
27
+ 跨项目固定用 `ledger/`(不占项目的 `docs/`);状态用 JSON 而非 Markdown(改动面最小);状态与过程正文分离(互不踩);一人一份分片把并发冲突面从整份文件缩到公共池里几行;**所有存盘时间(含过程文档里的日志行)一律 UTC**,脚本输出时统一转本机时区(人话与 `--json` 一致)。
28
28
 
29
29
  ## 怎么用(执行规则在 SKILL.md)
30
30
 
@@ -34,7 +34,7 @@
34
34
  python3 <SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工作区根>
35
35
  ```
36
36
 
37
- 子命令:`new` `claim` `set` `log` `close` `list` `history` `show` `validate`。命令参考(参数 / 一句话 / 举例)、任务生命周期、纪律(数据完整性 / 人类授权 / 故障处置)全在 `SKILL.md`;参数细节看 `<子命令> --help`(不要凭记忆编)。
37
+ 子命令:`new` `claim` `set` `log` `close` `list` `history` `show` `whoami` `validate`。命令参考(参数 / 一句话 / 举例)、任务生命周期、纪律(数据完整性 / 人类授权 / 故障处置)全在 `SKILL.md`;参数细节看 `<子命令> --help`(不要凭记忆编)。
38
38
 
39
39
  ## 深入参考(references/,按需读)
40
40
 
package/skill/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: task-ledger
3
3
  description: 本地任务台账,跨会话记录在办任务的归属、进度与起止。当用户要登记一件新的事(新建任务 / 建个坑 / 这活我领了)、记录已有任务的进展(接着做 / 记一条进展 / 这轮先记下)、查看任务状态或历史(现在该做什么 / 这活谁在做 / 上周做过什么)、给任务结案或退回(做完了 / 验收通过 / 不做了 / 退回待认领)、整理标签或检查台账一致性时加载;只是改代码、讨论方案时不要用。
4
- version: 1.3.22
4
+ version: 1.3.24
5
5
  allowed-tools: Read,Edit,Bash,Grep
6
6
  ---
7
7
 
@@ -21,11 +21,11 @@ allowed-tools: Read,Edit,Bash,Grep
21
21
 
22
22
  - **唯一能手写的是任务的过程文档(worklog)前五节**(怎么写见「过程文档」节);末节「执行记录」由脚本维护,**禁止手写、禁止手工添加**。
23
23
  - **其他所有文件一律不许手改**:修改 / 删除 / 任何操作都走命令;手改台账文件即数据损坏。
24
- - 台账看着不对劲(任务凭空少了、状态怪、分片对不上)⇒ **不要动手修**:让用户跑 `validate`(要连老月份和正文一起看就跑 `validate --deep`),把清单交给用户。
24
+ - 台账看着不对劲(任务凭空少了、状态怪、分片对不上)⇒ **不要动手修**:让用户跑 `validate`(老月份的历史也要查就跑 `validate --deep`),把清单交给用户。
25
25
 
26
26
  ## 命令参考
27
27
 
28
- 十个子命令:`init`(初始化本机身份,一台机器一次)+ 前五个写命令(`new`/`claim`/`set`/`log`/`close`)+ 后四个查询类(`list` / `show` / `history` / `validate`);何时用哪个见「任务生命周期」。参数语法看 `<子命令> --help`;下表说每个参数干什么、何时用。
28
+ 十一个子命令:`init`(初始化本机身份,一台机器一次)+ 前五个写命令(`new`/`claim`/`set`/`log`/`close`)+ 后五个只读命令(`list` / `show` / `history` / `whoami` / `validate`);何时用哪个见「任务生命周期」。参数语法看 `<子命令> --help`;下表说每个参数干什么、何时用。
29
29
 
30
30
  ### init — 初始化本机身份
31
31
 
@@ -110,7 +110,7 @@ allowed-tools: Read,Edit,Bash,Grep
110
110
  | `--status open\|doing` | 按状态筛(逗号分隔可多选;合法值只有 `open` / `doing`) |
111
111
  | `--category` / `--tag` | 按分类(精确)/ 标签(包含)筛 |
112
112
  | `--since` / `--until` | 按创建时间筛(只给日期含当天) |
113
- | `--json` | 输出 JSON(`{任务号: 任务对象}`) |
113
+ | `--json` | 输出 JSON(`{任务号: 任务对象}`);**每行带 `mine`**(是不是自己名下的,只比 uuid) |
114
114
 
115
115
  干什么:合并公共池 + 各人名下的任务,列出在办。**默认就是全量**,没有 `--all` 参数;收窄用 `--mine` / `--pool`。
116
116
 
@@ -120,10 +120,10 @@ allowed-tools: Read,Edit,Bash,Grep
120
120
 
121
121
  | 参数 | 含义 |
122
122
  |---|---|
123
- | `--id <任务号>` | 看哪条(必填;谁名下的都能看) |
124
- | `--json` | 连过程文档全文一起给(人话模式只给路径) |
123
+ | `--id <任务号>` | 看哪条(必填;谁名下的、在办还是已结案都能看) |
124
+ | `--json` | 任务字段 + 过程文档全文一起给(人话模式只给路径) |
125
125
 
126
- 干什么:拿任务 JSON + 过程文档路径 / 全文。改既有任务正文前,先用它拿路径。
126
+ 干什么:**看一条 = 任务字段 + 过程文档全文**,一次拿全;已结案的任务同样能看,不必先查 `history`。`--json` 里 `legacy: true` 表示这些字段是回退查在办得到的(老文档没有状态行)。
127
127
 
128
128
  举例:「这个任务到哪一步了」→ `show --id <任务号> --json`。
129
129
 
@@ -138,12 +138,32 @@ allowed-tools: Read,Edit,Bash,Grep
138
138
  | `--months N` | 最近 N 个自然月(与 `--since` / `--until` **互斥**,同传直接报错) |
139
139
  | `--limit N` | 最近 N 条 |
140
140
  | `--count-only` | 只输出 `{"count","capped"}`(与 `--json`、`--limit` 都互斥) |
141
- | `--json` | JSON 输出 |
141
+ | `--json` | JSON 输出;**每条记录带 `mine`**(是不是自己结的,只比 uuid) |
142
142
 
143
143
  干什么:按结案时间倒序排列,每条一行,只列结案时间、负责人、标题,其他都不在表格里;要摘要 / 分类 / 标签 / 完整时间加 `--json`。不给时间参数 = 最近 3 个自然月;最多 13 个月,超了直接报错。
144
144
 
145
145
  举例:「上周做过什么」→ `history --since <上周一日期>`;「今年都结掉哪些」→ `history --months 12`(超过 13 个月要分段查)。
146
146
 
147
+ ### whoami — 我是谁
148
+
149
+ | 参数 | 含义 |
150
+ |---|---|
151
+ | (无) | 打印本机身份:`@名字 uuid` |
152
+ | `--json` | `{"uuid","name"}`;**未初始化给双 null**(照常退 0,不是错误) |
153
+
154
+ 干什么:本机身份的只读查询,**不依赖 `--repo`、不写任何文件**,回答「这台机器上我是谁」。判断要不要引导用户 `init` 就看这条命令,别去解析 `~/.taskledger/identity.json`。
155
+
156
+ 举例:`whoami --json` 拿到 `uuid` 是 `null` ⇒ 这台机器还没初始化身份,先引导用户跑 `init`。
157
+
158
+ ### validate — 台账体检
159
+
160
+ | 参数 | 含义 |
161
+ |---|---|
162
+ | `--deep` | 历史读全部月份(默认只读当月那个月文件) |
163
+ | `--json` | `{"problems","notices","checked_files","total_tasks","deep"}` |
164
+
165
+ 干什么:只读体检,查公共池 + 各分片的字段格式、索引与文件对不对得上、历史格式。**不读过程文档正文**(浅度 / 深度都不读)。查出问题时退 1 —— 把清单原样交给用户,agent 不自行修。
166
+
147
167
  ## 怎么调用
148
168
 
149
169
  ```bash
@@ -152,7 +172,8 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
152
172
 
153
173
  - 定位项目:`--repo <工作区根>`(**直接含有 `ledger/` 的那个目录**)给了就校验后用;不给就从当前目录逐级向上找。`ledger/` 本身、以及它下面的任何子目录都不是根,指过去会被直接拒绝(判据只看位置,不看目录里有哪些文件 —— 项目根自带 `pool.json`、`history/`、`worklog/` 都不影响)。
154
174
  - `--no-sync`(放在子命令**前**):用户明确要求「先别同步」时才用;之后自己 `git add ledger && commit && push`。
155
- - `--json` 只对只读命令(`list` / `show` / `history` / `validate`)有意义;写命令不接收 `--json`,传了是用法错。`--json` 里的时间字段是 UTC,人话输出是本地时间。
175
+ - `--json` 只对只读命令(`list` / `show` / `history` / `whoami` / `validate`)有意义;写命令不接收 `--json`,传了是用法错。
176
+ - **时间**:存盘一律 UTC,脚本对外**一律给本机时区**(人话表格与 `--json` 都是),拿到直接显示,不要再转一次;`--since` / `--until` / `--created` / `--finished` 也按本机时区给,脚本内部转 UTC 比较。过程文档正文里的时间是原文,脚本不碰。
156
177
  - 命令回执只用来确认生效;任务状态以 `show` / `list` 为准。
157
178
 
158
179
  ## 任务生命周期
@@ -186,15 +207,24 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
186
207
 
187
208
  - 「执行记录」节名一个字都别动;老文档缺这一节也不用补。
188
209
  - 过程文档找不到时 `log` 只提示不失败;用 `show` 查路径,不要手写。
189
- - 头部只有创建时间 / 创建人、封卷标记、分类 / 标签行 —— **状态与归属以台账数据为准,别从头部读**。
190
- - 执行记录行格式 `- 本地时间 · 类型 · @谁 · 说明`,`@谁` 是这次动手的人、不是任务现归属;只追加、不改旧行。行首「类型」是脚本内部动作名(会见到 `release`,它就是 `set --release`);认不出就别当成新命令,按 `--help` 查。
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`),不用补。
220
+ - 执行记录行格式 `- 时间(UTC) · 类型 · @谁 · 说明`,`@谁` 是这次动手的人、不是任务现归属;只追加、不改旧行。行首「类型」是脚本内部动作名(会见到 `release`,它就是 `set --release`);认不出就别当成新命令,按 `--help` 查。
191
221
  - 只改某条任务的正文:`show --id <任务号>` 拿路径直接改,不另建 md;已结案的用 `history --months 12 --json` 找回链接(**人话输出里没有链接,必须加 `--json`**)——默认只查近 3 个月,结案久了的要显式放大窗口;只读不改。
192
222
 
193
223
  ### 收工三步(「收工」= 这一轮对用户有可见产出)
194
224
 
195
225
  1. 改正文:这轮变化的事实都落笔,「下一步」写具体动作。
196
226
  2. 跑 `log`:这一轮只要改过正文就必须记(一轮至少一条;纪律见下文)。正文没变也能记一条推进,但要同时重写「下一步」。
197
- 3. 回读末行(`tail -n 3 <路径>`):说明 = 这轮的事。执行记录行的时间是本机本地时间,别以为没记上。
227
+ 3. 回读末行(`tail -n 3 <路径>`):说明 = 这轮的事。执行记录行的时间是 **UTC**(比本机本地时间早若干小时,看时差别以为没记上)。
198
228
 
199
229
  **什么算一个节点**:明天自己想不起来、或换个人接手会看不出的事,就记一条;一次状态 / 结论变化对应一条,不对应一次工具调用。拿不准够不够格、`--note` 不会写 → 见 `references/log-note.md`。
200
230
 
@@ -217,7 +247,7 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
217
247
  | `new` / `claim` / `set` | 动手类 | 执行前复述完整参数并取得确认;**`new` 仅当用户原话点名要建且标题可直取原话时免复述**,标题需另拟或拿不准仍复述一次(`claim`/`set` 一律复述) |
218
248
  | `log` | 免确认 | 本任务的推进 / 卡点 / 交付记录直接跑,跑完一句话交代「已记执行记录:…」;要替用户下结论,先问(任务不在自己名下会直接被拒) |
219
249
  | `close` | 独立授权 | 仅认明确说「结束掉 / close 掉」(完整判定见生命周期第 8 条);说得含糊必须反问「是要结束这个任务吗?」;**agent 自判「干完了」不算** |
220
- | `list` / `show` / `history` / `validate` | 只读 | 直接跑 |
250
+ | `list` / `show` / `history` / `whoami` / `validate` | 只读 | 直接跑 |
221
251
 
222
252
  ### 故障处置(跑之后出事怎么办)
223
253
 
@@ -1,8 +1,11 @@
1
1
  # <id>:<title>
2
2
 
3
- > **创建**:<created>(@<creator>)
3
+ - **状态**:<status>
4
+ - **创建**:<created> · @<creator>
4
5
 
5
- <!-- 「> **分类**:… | **标签**:…」行由脚本按 new --category / --tags 插入;两者皆空则不写。 -->
6
+ <!-- 头部四行由脚本维护,顺序固定:分类/标签 → 状态 → 创建 → 封卷。
7
+ 「- **分类**:… · **标签**:…」按 new --category / --tags 插在最上面,两者皆空则不写;
8
+ 「- **封卷**:…」在结案时加在最后。 -->
6
9
 
7
10
  ## 背景
8
11
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-ledger",
3
- "version": "1.3.22",
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",
@@ -24,7 +24,12 @@ close → 历史层 + 封卷 + 移出自己名下
24
24
  - **只在补录过去时间时传**:`new --created`、`close --finished`。格式 `YYYY-MM-DD[ HH:MM[:SS]]`,按本地墙钟给、脚本内部转 UTC。`new --created` 补录时 `updated` 一并等于该时间 —— 之后补一条 `log` 把 `updated` 刷回当前。
25
25
  - 注意:任务号前缀与 worklog 月份目录按 **UTC** 推 —— UTC+8 下裸日期 `--created 2026-01-05` 存成 `2026-01-04 16:00:00`,任务号前缀变成 `20260104-1600`;临午夜补录要连时间一起传。
26
26
  - **筛选语义**:`list` 的 `--since` / `--until` 按**创建时间**;`history` 的按**结案时间**(历史文件按结案月归档,按创建时间比较会漏「上月建、本月结」的记录)。只给日期时起 = 当天 `00:00:00`、止 = 当天 `23:59:59`(含当天)。
27
- - **时区**:存盘为 UTC 的只有 —— 任务号、worklog 目录 / 文件名、台账 JSON 的 `created`/`started`/`updated`/`finished`、history 行、worklog 头部「创建」行。**执行记录行、封卷行、`list` / `history` 的人话输出都是本机本地时间**(给人看的日志行);`--json` 原样给 UTC,由消费方自己转。存量老数据(2026-10-22 前)写的是写入者本地时间、无标记,无法与 UTC 区分。
27
+ - **时区**:**存盘一律 UTC,出门一律本机时区**。
28
+ - 存盘:任务号、worklog 目录 / 文件名、台账 JSON 的 `created`/`started`/`updated`/`finished`、history 行、worklog 头部「创建」行与「状态」行、封卷行、每条执行记录行。
29
+ - 出门:`list` / `show` / `history` 的输出(人话表格与 `--json` 同一个值)都是本机时区,**拿到的就是能直接显示的**,不要再转。
30
+ - 进门:`--since` / `--until` / `--created` / `--finished` 按本机墙钟给,脚本内部转 UTC 再比较 / 落盘;除这四个参数没有任何地方要调用方传时间。
31
+ - **过程文档正文不动**:`show --json` 的 `worklog.text` 是原文,里面的时间是当时写进去的 UTC。
32
+ - 存量老数据写的是写入者本地时间、无标记,无法与 UTC 区分(JSON 是 2026-10-22 前,执行记录行是 2026-10-27 前)。
28
33
  - **history 文件 11 列**:结案时间 / 创建时间 / 工作包(= `new --title` 的任务标题)/ 分类 / 标签 / owner / owner_name / creator / creator_name / 摘要 / 过程文档;排序与 `--since` / `--until` 按**结案时间**。(`history` 命令的人话输出只展示其中几列:结案时间、负责人、标题。)
29
34
  - **owner / creator 两列存的是 uuid**(与任务里一致,身份只认 uuid);紧随其后的 `owner_name` / `creator_name` 两列**落盘**结案那一刻的显示名 —— 改名不回写旧行,看历史看到的就是当时那个名字。老行(升级前落的)没有名字时按名册兜底补一次展示值,**不回写文件**。
30
35
 
@@ -96,19 +96,19 @@
96
96
  - **「提示:当前目录不是 git 仓库,台账只写了本地文件」**:没有 git 也能用;想纳入版本管理就 `git init`,否则继续本地写(多人各写各的,后写覆盖先写)。
97
97
  - **「提示:任务 X 没有过程文档,已跳过封卷,历史末列留空」**:老数据没有 worklog 字段,结案照常,只是没有文档可封。
98
98
  - **`无需重复:这次没有新的改动。`**(`claim` / `set` / `close` 的 already_done 通道):上次已经做成过,重跑是幂等空转;`close` 重跑还会顺手补齐上次崩在半途的封卷与移出。
99
- - **`validate` 的 notices**(同号任务出现在多个文件、历史解析警告、worklog 仍是骨架等):只提示不判错,照实转述。
99
+ - **`validate` 的 notices**(同号任务出现在多个文件、历史解析警告等):只提示不判错,照实转述。
100
100
 
101
101
  ## validate 查什么(有问题 ⇒ 退出码 1)
102
102
 
103
- 两档:默认**浅度**只查格式与结构(账本 JSON + 当月历史 + 每篇过程文档的模板格式);加 **`--deep`** 才多读全部历史月份、并读正文判「还是不是空骨架」。
103
+ 两档:默认**浅度**只查格式与结构(账本 JSON + 当月历史);加 **`--deep`** 才多读全部历史月份。**两档都不读过程文档正文**(存量可达上万篇;过程文档是附件,格式与内容不在体检范围)。
104
104
 
105
- problems(任一即退 1):① 池里任务带着 owner;② 池里任务 status 不是 open/doing;③ 索引登记了身份但分片文件不在;④ 分片任务没有 owner;⑤ 分片任务 owner 与分片所属键不一致;⑥ 分片任务 status 非法;⑦ `owners/` 里有索引没登记的 `*.json`;⑧ 过程文档格式坏(第一行不是标题 / 头部缺「创建」行 / 前五节缺或顺序不对)。
106
- notices(只提示):过程文档缺「执行记录」节或有重复的节、历史解析警告、历史有结案行但任务还在办、还没有 `owners.json`;`--deep` 另加「过程文档还是空骨架」。
105
+ problems(任一即退 1):① 池里任务带着 owner;② 池里任务 status 不是 open/doing;③ 索引登记了身份但分片文件不在;④ 分片任务没有 owner;⑤ 分片任务 owner 与分片所属键不一致;⑥ 分片任务 status 非法;⑦ `owners/` 里有索引没登记的 `*.json`。
106
+ notices(只提示):历史解析警告、历史有结案行但任务还在办、还没有 `owners.json`。
107
107
  ①~⑦ 里结构类的属人工裁决:读原文确认哪份是真的,删多余那份或把索引改对,**不要用脚本猜**。索引或任一 JSON 损坏 ⇒ 退 3。
108
108
 
109
109
  ## 读命令只读本地(跨机器要先取最新)
110
110
 
111
- - **读命令(`list` / `show` / `history` / `validate`)不会自动 `git pull`**:看到的是**这台机器上一次同步后的**台账。
111
+ - **读命令(`list` / `show` / `history` / `whoami` / `validate`)不会自动 `git pull`**:看到的是**这台机器上一次同步后的**台账(`whoami` 连台账都不读,只看本机身份)。
112
112
  - 想看别的机器刚写的东西:先 `git pull`,或者跑任意一条写命令(写命令进入时会先取最新)。
113
113
 
114
114
  ## 写前取最新与写后同步(机制)
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from ..core.identity import Identity, normalize
8
8
  from ..core.paths import POOL_REL, shard_rel
9
9
  from ..core.time import now
10
- from ..core.worklog_md import exec_line
10
+ from ..core.worklog_md import StatusLine, exec_line
11
11
  from ..errors import Fail
12
12
  from ..store.owners_index import OwnersIndex
13
13
  from ..store.task_store import TaskStore
@@ -41,6 +41,7 @@ def claim(repo: Path, me: Identity, tid: str) -> Result:
41
41
  logs.append_exec(
42
42
  path,
43
43
  exec_line(now(), me.uuid, "claim", "认领(补齐上次未完成的清理)"),
44
+ status=StatusLine("doing", me.name, me.uuid, now()),
44
45
  )
45
46
  return Result(
46
47
  already_done=True,
@@ -72,7 +73,11 @@ def claim(repo: Path, me: Identity, tid: str) -> Result:
72
73
  logs = WorklogStore(repo)
73
74
  path = logs.find(tid, moved.get("worklog"))
74
75
  if path is not None:
75
- logs.append_exec(path, exec_line(now(), me.stamp(), "claim", "认领 → doing"))
76
+ logs.append_exec(
77
+ path,
78
+ exec_line(now(), me.stamp(), "claim", "认领 → doing"),
79
+ status=StatusLine("doing", me.name, me.uuid, now()),
80
+ )
76
81
  note = None
77
82
  return Result(
78
83
  changed=("owner", "started", "updated"),
@@ -12,10 +12,12 @@ from ..core.identity import Identity
12
12
  from ..core.labels import normalize_label, parse_tags
13
13
  from ..core.paths import history_path_for, shard_rel, to_repo_rel, worklog_name
14
14
  from ..core.worklog_md import (
15
+ StatusLine,
15
16
  append_exec_log,
16
17
  exec_line,
17
18
  has_frozen_line,
18
19
  rewrite_label_line,
20
+ rewrite_status_line,
19
21
  stamp_frozen,
20
22
  )
21
23
  from ..errors import Fail
@@ -99,6 +101,9 @@ def close_task(
99
101
  frozen = append_exec_log(
100
102
  frozen, exec_line(finished, me.stamp(), "close", "结案封卷")
101
103
  )
104
+ frozen = rewrite_status_line(
105
+ frozen, StatusLine("closed", me.name, me.uuid, finished)
106
+ )
102
107
  logs.write(doc_path, frozen)
103
108
  notes.append("补齐上次未完成的封卷。")
104
109
  if mine.get(tid) is not None:
@@ -160,6 +165,9 @@ def close_task(
160
165
  frozen = append_exec_log(
161
166
  frozen, exec_line(finished, me.stamp(), "close", "结案封卷")
162
167
  )
168
+ frozen = rewrite_status_line(
169
+ frozen, StatusLine("closed", me.name, me.uuid, finished)
170
+ )
163
171
  logs.write(doc_path, frozen)
164
172
 
165
173
  # ④ 删我分片那行(永远最后)
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from ..core.identity import Identity, normalize
8
8
  from ..core.paths import shard_rel
9
9
  from ..core.time import now
10
- from ..core.worklog_md import exec_line
10
+ from ..core.worklog_md import StatusLine, exec_line
11
11
  from ..errors import Fail
12
12
  from ..store.owners_index import OwnersIndex
13
13
  from ..store.task_store import TaskStore
@@ -62,6 +62,7 @@ def handoff(repo: Path, me: Identity, tid: str, *, to: str) -> Result:
62
62
  logs.append_exec(
63
63
  path,
64
64
  exec_line(now(), me.stamp(), "set", f"交接给 @{who_stamp}(补齐)"),
65
+ status=StatusLine("doing", to_name, to_key, now()),
65
66
  )
66
67
  return Result(
67
68
  already_done=True,
@@ -80,7 +81,9 @@ def handoff(repo: Path, me: Identity, tid: str, *, to: str) -> Result:
80
81
  note = f"任务 {tid} 已交给 {who},但过程文档没找到,跳过执行记录。"
81
82
  else:
82
83
  logs.append_exec(
83
- path, exec_line(now(), me.stamp(), "set", f"交接给 @{who_stamp}")
84
+ path,
85
+ exec_line(now(), me.stamp(), "set", f"交接给 @{who_stamp}"),
86
+ status=StatusLine("doing", to_name, to_key, now()),
84
87
  )
85
88
  return Result(
86
89
  changed=("owner", "owner_name", "updated"),
@@ -12,7 +12,7 @@ from ..core.paths import POOL_REL
12
12
  from ..errors import Fail
13
13
  from ..store.owners_index import OwnersIndex
14
14
  from ..store.task_store import TaskStore
15
- from .guard import in_scope, owner_of, require_uuid, status_of
15
+ from .guard import in_scope, is_mine, owner_of, require_uuid, status_of
16
16
  from .query import ListQuery
17
17
  from .result import ListResult, TaskRow, rel
18
18
 
@@ -52,6 +52,9 @@ def list_tasks(repo: Path, me: Identity, q: ListQuery) -> ListResult:
52
52
  kept = {
53
53
  tid: t for tid, t in rows.items() if in_scope(t, q, me=me, name_keys=name_keys)
54
54
  }
55
+ # 每行自带 `mine`(只比 uuid,零额外 IO):调用方不必自己拿 uuid 再比一次
56
+ for task in kept.values():
57
+ task["mine"] = is_mine(task, me)
55
58
  table = tuple(
56
59
  TaskRow(
57
60
  tid=tid,
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from ..core.identity import Identity
8
8
  from ..core.paths import shard_rel
9
9
  from ..core.time import now
10
- from ..core.worklog_md import exec_line
10
+ from ..core.worklog_md import StatusLine, exec_line
11
11
  from ..errors import Fail
12
12
  from ..store.task_store import TaskStore
13
13
  from ..store.worklog_store import WorklogStore
@@ -46,7 +46,11 @@ def log_progress(repo: Path, me: Identity, tid: str, note: str) -> Result:
46
46
  "提示:这条任务没有过程文档,已跳过执行记录(分片的最后更新已刷)。",
47
47
  ),
48
48
  )
49
- ok = logs.append_exec(path, exec_line(now(), me.stamp(), "log", text))
49
+ ok = logs.append_exec(
50
+ path,
51
+ exec_line(now(), me.stamp(), "log", text),
52
+ status=StatusLine("doing", me.name, me.uuid, now()),
53
+ )
50
54
  if not ok:
51
55
  return Result(
52
56
  changed=("updated",),
@@ -17,7 +17,7 @@ from ..core.paths import (
17
17
  to_repo_rel,
18
18
  worklog_rel,
19
19
  )
20
- from ..core.worklog_md import exec_line
20
+ from ..core.worklog_md import StatusLine, exec_line
21
21
  from ..errors import Fail
22
22
  from ..store.atomic import ensure_dir, restore, snapshot
23
23
  from ..store.task_store import TaskStore
@@ -102,6 +102,12 @@ def new_task(
102
102
  creator=me.stamp(),
103
103
  category=category,
104
104
  tags=tag_list,
105
+ status=StatusLine(
106
+ "doing" if mine else "open",
107
+ me.name if mine else None,
108
+ me.uuid if mine else None,
109
+ created,
110
+ ),
105
111
  first_exec=exec_line(
106
112
  created, me.stamp(), "new", "创建任务并认领" if mine else "创建任务"
107
113
  ),
@@ -18,7 +18,7 @@ from ..core.time import now, time_key
18
18
  from ..errors import Fail
19
19
  from ..store.history_store import HistoryStore
20
20
  from ..store.owners_index import OwnersIndex
21
- from .guard import require_uuid
21
+ from .guard import is_mine, require_uuid
22
22
  from .query import COUNT_CAP, HistoryQuery
23
23
  from .result import HistoryResult, rel
24
24
 
@@ -132,6 +132,8 @@ def query_history(repo: Path, me: Identity, q: HistoryQuery) -> HistoryResult:
132
132
  row["src"] = rel(repo, path)
133
133
  row["created"] = time_key(row.get("created") or "") or row.get("created")
134
134
  row["finished"] = time_key(row.get("finished") or "") or row.get("finished")
135
+ # 每行自带 `mine`(只比 uuid):调用方不必自己拿 uuid 再比一次
136
+ row["mine"] = is_mine(row, me)
135
137
  records.append(row)
136
138
 
137
139
  index = OwnersIndex(repo)