dsh-task-ledger 0.0.95 → 0.0.97
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 +3618 -116
- package/dist/client.js.map +1 -1
- package/dist/host/repositories/ledger.repository.d.ts +8 -2
- package/dist/host/repositories/ledger.repository.js +4 -1
- package/dist/host/repositories/records.d.ts +10 -2
- package/dist/host/repositories/records.js +11 -2
- package/dist/host/routes.js +1 -0
- package/dist/host/services/dates.d.ts +7 -2
- package/dist/host/services/dates.js +29 -6
- package/dist/host/services/history.service.js +1 -1
- package/dist/host/services/trend.service.d.ts +4 -1
- package/dist/host/services/trend.service.js +9 -1
- package/package.json +4 -1
- package/skill/README.md +2 -2
- package/skill/SKILL.md +34 -13
- package/skill/assets/worklog.md +1 -0
- package/skill/package.json +1 -1
- package/skill/references/state-model.md +6 -1
- package/skill/references/troubleshooting.md +5 -5
- package/skill/scripts/ledgerkit/actions/claim.py +7 -2
- package/skill/scripts/ledgerkit/actions/close_task.py +8 -0
- package/skill/scripts/ledgerkit/actions/handoff.py +5 -2
- package/skill/scripts/ledgerkit/actions/list_tasks.py +4 -1
- package/skill/scripts/ledgerkit/actions/log_progress.py +6 -2
- package/skill/scripts/ledgerkit/actions/new_task.py +7 -1
- package/skill/scripts/ledgerkit/actions/query_history.py +3 -1
- package/skill/scripts/ledgerkit/actions/release.py +6 -2
- package/skill/scripts/ledgerkit/actions/result.py +2 -0
- package/skill/scripts/ledgerkit/actions/show_task.py +31 -4
- package/skill/scripts/ledgerkit/actions/validate.py +6 -49
- package/skill/scripts/ledgerkit/core/history_md.py +18 -13
- package/skill/scripts/ledgerkit/core/time.py +2 -1
- package/skill/scripts/ledgerkit/core/worklog_md.py +137 -8
- package/skill/scripts/ledgerkit/shell/cli.py +24 -2
- package/skill/scripts/ledgerkit/shell/render.py +48 -4
- package/skill/scripts/ledgerkit/store/worklog_store.py +15 -4
|
@@ -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
|
-
/**
|
|
23
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
5
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 列的键 → 领域字段名。
|
package/dist/host/routes.js
CHANGED
|
@@ -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`)→
|
|
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
|
-
/**
|
|
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
|
|
17
|
-
|
|
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 =
|
|
55
|
-
if (
|
|
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
|
-
.
|
|
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 {
|
|
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.
|
|
3
|
+
"version": "0.0.97",
|
|
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
|
|
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.
|
|
4
|
+
version: 1.3.23
|
|
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
|
|
24
|
+
- 台账看着不对劲(任务凭空少了、状态怪、分片对不上)⇒ **不要动手修**:让用户跑 `validate`(老月份的历史也要查就跑 `validate --deep`),把清单交给用户。
|
|
25
25
|
|
|
26
26
|
## 命令参考
|
|
27
27
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
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,15 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
|
|
|
186
207
|
|
|
187
208
|
- 「执行记录」节名一个字都别动;老文档缺这一节也不用补。
|
|
188
209
|
- 过程文档找不到时 `log` 只提示不失败;用 `show` 查路径,不要手写。
|
|
189
|
-
-
|
|
190
|
-
- 执行记录行格式 `-
|
|
210
|
+
- 头部由脚本维护:创建时间 / 创建人、状态行(`> **状态**:doing · @某人(uuid)· 更新 <时间>`)、封卷标记、分类 / 标签行 —— **都不要手改**。老文档没有状态行是合法的(`show` 回退查在办,`--json` 标 `legacy: true`),不用补。
|
|
211
|
+
- 执行记录行格式 `- 时间(UTC) · 类型 · @谁 · 说明`,`@谁` 是这次动手的人、不是任务现归属;只追加、不改旧行。行首「类型」是脚本内部动作名(会见到 `release`,它就是 `set --release`);认不出就别当成新命令,按 `--help` 查。
|
|
191
212
|
- 只改某条任务的正文:`show --id <任务号>` 拿路径直接改,不另建 md;已结案的用 `history --months 12 --json` 找回链接(**人话输出里没有链接,必须加 `--json`**)——默认只查近 3 个月,结案久了的要显式放大窗口;只读不改。
|
|
192
213
|
|
|
193
214
|
### 收工三步(「收工」= 这一轮对用户有可见产出)
|
|
194
215
|
|
|
195
216
|
1. 改正文:这轮变化的事实都落笔,「下一步」写具体动作。
|
|
196
217
|
2. 跑 `log`:这一轮只要改过正文就必须记(一轮至少一条;纪律见下文)。正文没变也能记一条推进,但要同时重写「下一步」。
|
|
197
|
-
3. 回读末行(`tail -n 3 <路径>`):说明 =
|
|
218
|
+
3. 回读末行(`tail -n 3 <路径>`):说明 = 这轮的事。执行记录行的时间是 **UTC**(比本机本地时间早若干小时,看时差别以为没记上)。
|
|
198
219
|
|
|
199
220
|
**什么算一个节点**:明天自己想不起来、或换个人接手会看不出的事,就记一条;一次状态 / 结论变化对应一条,不对应一次工具调用。拿不准够不够格、`--note` 不会写 → 见 `references/log-note.md`。
|
|
200
221
|
|
|
@@ -217,7 +238,7 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工
|
|
|
217
238
|
| `new` / `claim` / `set` | 动手类 | 执行前复述完整参数并取得确认;**`new` 仅当用户原话点名要建且标题可直取原话时免复述**,标题需另拟或拿不准仍复述一次(`claim`/`set` 一律复述) |
|
|
218
239
|
| `log` | 免确认 | 本任务的推进 / 卡点 / 交付记录直接跑,跑完一句话交代「已记执行记录:…」;要替用户下结论,先问(任务不在自己名下会直接被拒) |
|
|
219
240
|
| `close` | 独立授权 | 仅认明确说「结束掉 / close 掉」(完整判定见生命周期第 8 条);说得含糊必须反问「是要结束这个任务吗?」;**agent 自判「干完了」不算** |
|
|
220
|
-
| `list` / `show` / `history` / `validate` | 只读 | 直接跑 |
|
|
241
|
+
| `list` / `show` / `history` / `whoami` / `validate` | 只读 | 直接跑 |
|
|
221
242
|
|
|
222
243
|
### 故障处置(跑之后出事怎么办)
|
|
223
244
|
|
package/skill/assets/worklog.md
CHANGED
package/skill/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-ledger",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.23",
|
|
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
|
-
-
|
|
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
|
|
99
|
+
- **`validate` 的 notices**(同号任务出现在多个文件、历史解析警告等):只提示不判错,照实转述。
|
|
100
100
|
|
|
101
101
|
## validate 查什么(有问题 ⇒ 退出码 1)
|
|
102
102
|
|
|
103
|
-
两档:默认**浅度**只查格式与结构(账本 JSON +
|
|
103
|
+
两档:默认**浅度**只查格式与结构(账本 JSON + 当月历史);加 **`--deep`** 才多读全部历史月份。**两档都不读过程文档正文**(存量可达上万篇;过程文档是附件,格式与内容不在体检范围)。
|
|
104
104
|
|
|
105
|
-
problems(任一即退 1):① 池里任务带着 owner;② 池里任务 status 不是 open/doing;③ 索引登记了身份但分片文件不在;④ 分片任务没有 owner;⑤ 分片任务 owner 与分片所属键不一致;⑥ 分片任务 status 非法;⑦ `owners/` 里有索引没登记的 `*.json
|
|
106
|
-
notices
|
|
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(
|
|
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,
|
|
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(
|
|
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)
|