dsh-task-ledger 0.0.101 → 0.0.103

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 (71) hide show
  1. package/dist/client.js +3 -3
  2. package/dist/client.js.map +1 -1
  3. package/dist/host/errors.d.ts +2 -2
  4. package/dist/host/errors.js +2 -2
  5. package/dist/host/repositories/runner.d.ts +2 -2
  6. package/dist/host/repositories/runner.js +13 -10
  7. package/dist/host/services/session.service.js +1 -1
  8. package/dist/host/skill/registration.d.ts +2 -1
  9. package/dist/host/skill/registration.js +4 -3
  10. package/package.json +2 -2
  11. package/skill/README.md +4 -2
  12. package/skill/SKILL.md +5 -3
  13. package/skill/assets/worklog.md +1 -1
  14. package/skill/package.json +1 -1
  15. package/skill/references/init-identity.md +3 -3
  16. package/skill/references/log-note.md +1 -1
  17. package/skill/references/troubleshooting.md +1 -1
  18. package/skill/scripts/bin/ledger-darwin-arm64 +0 -0
  19. package/skill/scripts/bin/ledger-linux-amd64 +0 -0
  20. package/skill/scripts/bin/ledger-windows-amd64.exe +0 -0
  21. package/skill/scripts/progress +44 -0
  22. package/skill/scripts/ledgerkit/__init__.py +0 -0
  23. package/skill/scripts/ledgerkit/actions/__init__.py +0 -0
  24. package/skill/scripts/ledgerkit/actions/atom.py +0 -154
  25. package/skill/scripts/ledgerkit/actions/bootstrap.py +0 -37
  26. package/skill/scripts/ledgerkit/actions/claim.py +0 -88
  27. package/skill/scripts/ledgerkit/actions/close_task.py +0 -191
  28. package/skill/scripts/ledgerkit/actions/guard.py +0 -158
  29. package/skill/scripts/ledgerkit/actions/handoff.py +0 -93
  30. package/skill/scripts/ledgerkit/actions/init_identity.py +0 -90
  31. package/skill/scripts/ledgerkit/actions/list_tasks.py +0 -74
  32. package/skill/scripts/ledgerkit/actions/log_progress.py +0 -67
  33. package/skill/scripts/ledgerkit/actions/new_task.py +0 -129
  34. package/skill/scripts/ledgerkit/actions/query.py +0 -35
  35. package/skill/scripts/ledgerkit/actions/query_history.py +0 -220
  36. package/skill/scripts/ledgerkit/actions/registry.py +0 -82
  37. package/skill/scripts/ledgerkit/actions/relabel.py +0 -89
  38. package/skill/scripts/ledgerkit/actions/release.py +0 -76
  39. package/skill/scripts/ledgerkit/actions/result.py +0 -90
  40. package/skill/scripts/ledgerkit/actions/set_task.py +0 -71
  41. package/skill/scripts/ledgerkit/actions/show_task.py +0 -55
  42. package/skill/scripts/ledgerkit/actions/validate.py +0 -189
  43. package/skill/scripts/ledgerkit/core/__init__.py +0 -0
  44. package/skill/scripts/ledgerkit/core/history_md.py +0 -152
  45. package/skill/scripts/ledgerkit/core/identity.py +0 -54
  46. package/skill/scripts/ledgerkit/core/ids.py +0 -58
  47. package/skill/scripts/ledgerkit/core/labels.py +0 -89
  48. package/skill/scripts/ledgerkit/core/paths.py +0 -92
  49. package/skill/scripts/ledgerkit/core/roots.py +0 -74
  50. package/skill/scripts/ledgerkit/core/time.py +0 -103
  51. package/skill/scripts/ledgerkit/core/worklog_md.py +0 -435
  52. package/skill/scripts/ledgerkit/env.py +0 -94
  53. package/skill/scripts/ledgerkit/errors.py +0 -15
  54. package/skill/scripts/ledgerkit/notices.py +0 -28
  55. package/skill/scripts/ledgerkit/shell/__init__.py +0 -0
  56. package/skill/scripts/ledgerkit/shell/cli.py +0 -532
  57. package/skill/scripts/ledgerkit/shell/gitops.py +0 -668
  58. package/skill/scripts/ledgerkit/shell/merge.py +0 -266
  59. package/skill/scripts/ledgerkit/shell/presync.py +0 -23
  60. package/skill/scripts/ledgerkit/shell/render.py +0 -216
  61. package/skill/scripts/ledgerkit/shell/runner.py +0 -232
  62. package/skill/scripts/ledgerkit/store/__init__.py +0 -0
  63. package/skill/scripts/ledgerkit/store/atomic.py +0 -130
  64. package/skill/scripts/ledgerkit/store/dirty.py +0 -108
  65. package/skill/scripts/ledgerkit/store/history_store.py +0 -134
  66. package/skill/scripts/ledgerkit/store/json_store.py +0 -166
  67. package/skill/scripts/ledgerkit/store/locks.py +0 -64
  68. package/skill/scripts/ledgerkit/store/owners_index.py +0 -127
  69. package/skill/scripts/ledgerkit/store/task_store.py +0 -28
  70. package/skill/scripts/ledgerkit/store/worklog_store.py +0 -178
  71. package/skill/scripts/progress.py +0 -20
@@ -2,10 +2,10 @@
2
2
  * 横切:错误模型与降级语义(2026-10-18 重构步骤 3 落地)。
3
3
  *
4
4
  * 两种失败分开表达:
5
- * - **已知失败**(坏数据 / 脚本非零 / python3 起不来 / 超时):service 层捕获进 `degraded`,
5
+ * - **已知失败**(坏数据 / 脚本非零 / 台账程序起不来 / 超时):service 层捕获进 `degraded`,
6
6
  * 接口仍 200,该工作区被前端标红,其他工作区不受影响(不再一抛全板 500);
7
7
  * - **全局失败**(所有工作区都读不到 / 编程错误):照旧抛,路由转 500 + 人话。
8
- * 防止「python3 根本没装」被集体降级成一片绿色空板——那等于界面撒谎。
8
+ * 防止「台账程序根本没装」被集体降级成一片绿色空板——那等于界面撒谎。
9
9
  */
10
10
  /** 机器可读错误码(只增不改;`LEDGER_` 前缀避开宿主错误码空间)。 */
11
11
  export type LedgerErrorCode = "LEDGER_SCRIPT_UNAVAILABLE" | "LEDGER_SCRIPT_TIMEOUT" | "LEDGER_SCRIPT_FAILED" | "LEDGER_SCRIPT_BAD_JSON" | "LEDGER_DATA_INVALID";
@@ -2,10 +2,10 @@
2
2
  * 横切:错误模型与降级语义(2026-10-18 重构步骤 3 落地)。
3
3
  *
4
4
  * 两种失败分开表达:
5
- * - **已知失败**(坏数据 / 脚本非零 / python3 起不来 / 超时):service 层捕获进 `degraded`,
5
+ * - **已知失败**(坏数据 / 脚本非零 / 台账程序起不来 / 超时):service 层捕获进 `degraded`,
6
6
  * 接口仍 200,该工作区被前端标红,其他工作区不受影响(不再一抛全板 500);
7
7
  * - **全局失败**(所有工作区都读不到 / 编程错误):照旧抛,路由转 500 + 人话。
8
- * 防止「python3 根本没装」被集体降级成一片绿色空板——那等于界面撒谎。
8
+ * 防止「台账程序根本没装」被集体降级成一片绿色空板——那等于界面撒谎。
9
9
  */
10
10
  /** 台账数据形状不符(容器级校验失败)。message 带工作区名 + 人话,可直接上界面。 */
11
11
  export class LedgerDataError extends Error {
@@ -36,7 +36,7 @@ export interface RunOptions {
36
36
  cwd?: string;
37
37
  }
38
38
  /**
39
- * 读命令:spawn python3 跑内嵌 skill 脚本,收 stdout JSON。
39
+ * 读命令:跑内嵌台账程序,收 stdout JSON。
40
40
  * 不解析任何台账格式——解析真源只有 skill 脚本一份(2026-10-17 用户拍板)。
41
41
  *
42
42
  * @param sub - 子命令(list / history / show…)
@@ -47,7 +47,7 @@ export interface RunOptions {
47
47
  */
48
48
  export declare function runLedgerScript<T = unknown>(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerScriptResult<T>>;
49
49
  /**
50
- * 写命令(claim / set):同样是 spawn python3,但**不解析 JSON**——这些子命令打的是人话,
50
+ * 写命令(claim / set):同样是 spawn 入口脚本,但**不解析 JSON**——这些子命令打的是人话,
51
51
  * 成功与否看退出码,失败时 stderr 就是能直接上界面的人话。
52
52
  */
53
53
  export declare function runLedgerCommand(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerCommandResult>;
@@ -1,11 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  /**
4
- * 内嵌 skill 脚本的默认路径:dist/host/repositories/runner.js → 包根 skill/scripts/progress.py
4
+ * 内嵌 skill 入口的默认路径:dist/host/repositories/runner.js → 包根 skill/scripts/progress
5
5
  * (比原 host/script.js 深一层 ⇒ 相对路径多一级 `../`)。
6
- * 构建时 scripts/copy-skill.mjs 把 packages/skill 拷到 packages/plugin/skill/。
6
+ *
7
+ * 这是个薄 shell 包装,按平台 exec `skill/scripts/bin/ledger-<os>-<arch>`(自带运行时的
8
+ * 静态二进制)——所以本插件不需要系统装任何运行时。
9
+ * 构建时 scripts/copy-skill.mjs 把 packages/skill 的运行时拷到 packages/plugin/skill/。
7
10
  */
8
- const DEFAULT_SCRIPT_PATH = fileURLToPath(new URL("../../../skill/scripts/progress.py", import.meta.url));
11
+ const DEFAULT_SCRIPT_PATH = fileURLToPath(new URL("../../../skill/scripts/progress", import.meta.url));
9
12
  /** 单次脚本调用超时上限(ms)。实测单仓 5 万行全量 1.2s,10s 是宽松上限。 */
10
13
  const DEFAULT_TIMEOUT_MS = 10_000;
11
14
  /**
@@ -16,14 +19,14 @@ const DEFAULT_TIMEOUT_MS = 10_000;
16
19
  */
17
20
  export const WRITE_TIMEOUT_MS = 180_000;
18
21
  /**
19
- * 起一次 python3、收 stdout / stderr。出错一律归一成人话(起不了 python3 / 超时 / 非零退出)。
22
+ * 起一次台账程序、收 stdout / stderr。出错一律归一成人话(起不了程序 / 超时 / 非零退出)。
20
23
  * 解析留给调用方:读命令要 JSON,写命令要原文。
21
24
  */
22
25
  function exec(sub, args, opts) {
23
26
  const scriptPath = opts?.scriptPath ?? DEFAULT_SCRIPT_PATH;
24
27
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
25
28
  return new Promise((resolve) => {
26
- const child = spawn("python3", [scriptPath, sub, ...args], {
29
+ const child = spawn(scriptPath, [sub, ...args], {
27
30
  cwd: opts?.cwd,
28
31
  stdio: ["ignore", "pipe", "pipe"],
29
32
  });
@@ -53,14 +56,14 @@ function exec(sub, args, opts) {
53
56
  child.on("error", (error) => {
54
57
  settle({
55
58
  ok: false,
56
- error: `起不了 python3(${error.message})——本插件需要 python3 运行内嵌 skill 脚本`,
59
+ error: `起不了台账程序(${error.message})——内嵌 skill 的二进制可能缺失或不可执行`,
57
60
  });
58
61
  });
59
62
  child.on("close", (code) => {
60
63
  if (code !== 0) {
61
64
  settle({
62
65
  ok: false,
63
- error: stderr.trim() || `skill 脚本退出码 ${String(code)}`,
66
+ error: stderr.trim() || `台账程序退出码 ${String(code)}`,
64
67
  });
65
68
  return;
66
69
  }
@@ -69,7 +72,7 @@ function exec(sub, args, opts) {
69
72
  });
70
73
  }
71
74
  /**
72
- * 读命令:spawn python3 跑内嵌 skill 脚本,收 stdout JSON。
75
+ * 读命令:跑内嵌台账程序,收 stdout JSON。
73
76
  * 不解析任何台账格式——解析真源只有 skill 脚本一份(2026-10-17 用户拍板)。
74
77
  *
75
78
  * @param sub - 子命令(list / history / show…)
@@ -93,7 +96,7 @@ export async function runLedgerScript(sub, args, opts) {
93
96
  }
94
97
  }
95
98
  /**
96
- * 写命令(claim / set):同样是 spawn python3,但**不解析 JSON**——这些子命令打的是人话,
99
+ * 写命令(claim / set):同样是 spawn 入口脚本,但**不解析 JSON**——这些子命令打的是人话,
97
100
  * 成功与否看退出码,失败时 stderr 就是能直接上界面的人话。
98
101
  */
99
102
  export async function runLedgerCommand(sub, args, opts) {
@@ -101,7 +104,7 @@ export async function runLedgerCommand(sub, args, opts) {
101
104
  if (!result.ok)
102
105
  return result;
103
106
  const output = result.stdout.trim();
104
- // 退出码 0 但 stderr 有字 = 写成功、同步失败(progress.py 的既定行为),必须带出去
107
+ // 退出码 0 但 stderr 有字 = 写成功、同步失败(台账程序的既定行为),必须带出去
105
108
  return result.stderr
106
109
  ? { ok: true, output, notice: result.stderr }
107
110
  : { ok: true, output };
@@ -50,7 +50,7 @@ export function buildClosePrompt(input) {
50
50
  "请按顺序做完这三件,然后停下:",
51
51
  "1. 读过程文档的「背景 / 待办 / 下一步」,核对这次到底做了什么、还留了什么坑。",
52
52
  `2. 用 task-ledger skill 的脚本结案(在工作区目录下、原样执行这一条命令):`,
53
- ` python3 ${SKILL_SCRIPT} close --id ${input.taskId} --repo ${input.workspacePath} --summary "<一句话说清做成了什么、留了什么坑>"`,
53
+ ` ${SKILL_SCRIPT} close --id ${input.taskId} --repo ${input.workspacePath} --summary "<一句话说清做成了什么、留了什么坑>"`,
54
54
  " `--summary` 是必填参数,不许编套话;命令报错就原样报告错误,不要自己绕过、不要手改台账文件。",
55
55
  "3. 收尾即结束——这就是本条消息的全部要求。",
56
56
  "",
@@ -1,4 +1,5 @@
1
- /** skill 脚本绝对路径(提示词里直接给「原样执行这一条」的命令用;与注册共用同一份目录知识)。 */
1
+ /** 台账入口绝对路径(提示词里直接给「原样执行这一条」的命令用;与注册共用同一份目录知识)。
2
+ * 它是按平台选二进制的薄包装,不需要系统装任何运行时。 */
2
3
  export declare const SKILL_SCRIPT: string;
3
4
  /** 宿主 ctx.skills.register 的最小面(外部事实文档的结构面)。 */
4
5
  export interface SkillRegistration {
@@ -9,8 +9,9 @@ import { fileURLToPath } from "node:url";
9
9
  const SKILL_DIR = fileURLToPath(new URL("../../../skill", import.meta.url));
10
10
  /** SKILL.md 里指示脚本位置的占位符(注册时替换为绝对路径,模型据此找到脚本)。 */
11
11
  const SKILL_DIR_TOKEN = "<本 SKILL.md 所在目录>";
12
- /** skill 脚本绝对路径(提示词里直接给「原样执行这一条」的命令用;与注册共用同一份目录知识)。 */
13
- export const SKILL_SCRIPT = `${SKILL_DIR}/scripts/progress.py`;
12
+ /** 台账入口绝对路径(提示词里直接给「原样执行这一条」的命令用;与注册共用同一份目录知识)。
13
+ * 它是按平台选二进制的薄包装,不需要系统装任何运行时。 */
14
+ export const SKILL_SCRIPT = `${SKILL_DIR}/scripts/progress`;
14
15
  /** frontmatter 最小解析:只取 name / description(不引 YAML 库;值去首尾引号)。 */
15
16
  function parseFrontmatter(text) {
16
17
  const match = /^---\n([\s\S]*?)\n---/.exec(text);
@@ -44,7 +45,7 @@ export function buildSkillRegistration(skillDir = SKILL_DIR) {
44
45
  if (content.includes(SKILL_DIR_TOKEN)) {
45
46
  throw new Error("占位符替换失败:SKILL.md 里仍残留脚本路径占位符");
46
47
  }
47
- if (!content.includes(`${skillDir}/scripts/progress.py`)) {
48
+ if (!content.includes(`${skillDir}/scripts/progress`)) {
48
49
  throw new Error("占位符替换异常:替换后找不到脚本路径");
49
50
  }
50
51
  return { name, description, content, path: `${skillDir}/SKILL.md` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-task-ledger",
3
- "version": "0.0.101",
3
+ "version": "0.0.103",
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",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "build": "npm run build:host && npm run build:client",
28
- "build:host": "node scripts/clean-dist.mjs && tsc && node scripts/sync-skill-version.mjs && node scripts/copy-skill.mjs",
28
+ "build:host": "node scripts/clean-dist.mjs && tsc && node scripts/copy-skill.mjs",
29
29
  "build:client": "tsc --noEmit -p tsconfig.client.json && tsdown --config tsdown.client.config.ts",
30
30
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.client.json",
31
31
  "verify": "npm run build && npm run typecheck"
package/skill/README.md CHANGED
@@ -31,9 +31,11 @@
31
31
  直接跑:
32
32
 
33
33
  ```bash
34
- python3 <SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工作区根>
34
+ <SKILL.md 所在目录>/scripts/progress <子命令> --repo <工作区根>
35
35
  ```
36
36
 
37
+ `scripts/progress` 按平台 exec `scripts/bin/ledger-<os>-<arch>`(静态二进制;已带 linux/amd64、darwin/arm64 与 windows/amd64)。
38
+
37
39
  子命令:`new` `claim` `set` `log` `close` `list` `history` `show` `whoami` `validate`。命令参考(参数 / 一句话 / 举例)、任务生命周期、纪律(数据完整性 / 人类授权 / 故障处置)全在 `SKILL.md`;参数细节看 `<子命令> --help`(不要凭记忆编)。
38
40
 
39
41
  ## 深入参考(references/,按需读)
@@ -46,4 +48,4 @@ python3 <SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工作
46
48
 
47
49
  ## 本仓自己的台账
48
50
 
49
- 本仓库(`dsh-task-ledger`)也用 ledgerkit 管自己的活,台账数据在 `ledger/`;它和本 skill 源码 `packages/skill/` 是两回事 —— **改 skill 请别动 `ledger/`**。
51
+ 本仓库(`dsh-task-ledger`)也用自己的这套脚本(`scripts/progress` → `src/` 编译出的二进制)管自己的活,台账数据在 `ledger/`;它和本 skill 源码 `packages/skill/` 是两回事 —— **改 skill 请别动 `ledger/`**。
package/skill/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: task-ledger
3
3
  description: 本地任务台账,跨会话记录在办任务的归属、进度与起止。当用户要登记一件新的事(新建任务 / 建个坑 / 这活我领了)、记录已有任务的进展(接着做 / 记一条进展 / 这轮先记下)、查看任务状态或历史(现在该做什么 / 这活谁在做 / 上周做过什么)、给任务结案或退回(做完了 / 验收通过 / 不做了 / 退回待认领)、整理标签或检查台账一致性时加载;只是改代码、讨论方案时不要用。
4
- version: 1.3.27
4
+ version: 1.3.30
5
5
  allowed-tools: Read,Edit,Bash,Grep
6
6
  ---
7
7
 
@@ -92,7 +92,7 @@ allowed-tools: Read,Edit,Bash,Grep
92
92
 
93
93
  干什么:刷任务的最后进度时间 + 在过程文档「执行记录」节追加一行。改正文必配一条 log(见纪律)。
94
94
 
95
- 举例:一轮干完 → `log --id <任务号> --note "改了认领即 doing(claim.py:56);下一步同步文档"`。
95
+ 举例:一轮干完 → `log --id <任务号> --note "改了认领即 doing(internal/actions/claim.go:56);下一步同步文档"`。
96
96
 
97
97
  ### close — 结案
98
98
 
@@ -175,9 +175,11 @@ allowed-tools: Read,Edit,Bash,Grep
175
175
  ## 怎么调用
176
176
 
177
177
  ```bash
178
- python3 <本 SKILL.md 所在目录>/scripts/progress.py <子命令> --repo <工作区根>
178
+ <本 SKILL.md 所在目录>/scripts/progress <子命令> --repo <工作区根>
179
179
  ```
180
180
 
181
+ `scripts/progress` 是个薄 shell 包装,按平台 exec `scripts/bin/ledger-<os>-<arch>`(自带运行时的静态二进制;已提供 linux/amd64、darwin/arm64 与 windows/amd64)。平台或架构没有对应产物时,它会明确报错,不会静默失败。
182
+
181
183
  - 定位项目:`--repo <工作区根>`(**直接含有 `ledger/` 的那个目录**)给了就校验后用;不给就从当前目录逐级向上找。`ledger/` 本身、以及它下面的任何子目录都不是根,指过去会被直接拒绝(判据只看位置,不看目录里有哪些文件 —— 项目根自带 `pool.json`、`history/`、`worklog/` 都不影响)。
182
184
  - `--no-sync`(放在子命令**前**):用户明确要求「先别同步」时才用;之后自己 `git add ledger && commit && push`。
183
185
  - `--json` 只对只读命令(`list` / `show` / `history` / `whoami` / `validate`)有意义;写命令不接收 `--json`,传了是用法错。
@@ -30,4 +30,4 @@
30
30
  ## 执行记录
31
31
 
32
32
  <!-- 本节由脚本维护:new / claim / set / release / log / close 自动追加,请勿手改。
33
- 补记用 progress.py log --id <任务号> --note "一句话"。 -->
33
+ 补记用 progress log --id <任务号> --note "一句话"。 -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-ledger",
3
- "version": "1.3.27",
3
+ "version": "1.3.30",
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",
@@ -11,10 +11,10 @@
11
11
  1. 只读报名单(你在非终端里裸跑,只会打印候选、零写):
12
12
 
13
13
  ```
14
- python3 <本 SKILL.md 所在目录>/scripts/progress.py init --repo <工作区根>
14
+ <本 SKILL.md 所在目录>/scripts/progress init --repo <工作区根>
15
15
  ```
16
16
 
17
- 2. 把名册给用户选,并把这**同一条命令**交给用户在 ta 自己的终端跑(真终端里它自己会变成 ↑/↓ 选择框):
17
+ 2. 把名册给用户选,并把这**同一条命令**交给用户在 ta 自己的终端跑(真终端里它自己会变成 ↑/↓ 选择框:↑/↓ 移动、Enter 确认、末尾「+ 新建身份」输入名字、最后 Y/N 确认、ESC/Ctrl-C 取消):
18
18
  - 名册有内容 ⇒「名册现有:@A(uuid)、@B(uuid)。请在你的终端运行上面这条,它会让你选。」
19
19
  - 名册为空 ⇒「名册为空。请在你的终端运行上面这条,输入一个用户名新建。」
20
20
 
@@ -26,7 +26,7 @@ python3 <本 SKILL.md 所在目录>/scripts/progress.py init --repo <工作区
26
26
 
27
27
  ## 硬规则
28
28
 
29
- - **不诊断、不排查**:不要 `ls ~/.taskledger`、不要 `cat identity.json`、不要读 skill 源码(`env.py` / `identity_file.py` 等)或台账去查「为什么没身份」。缺就是未初始化,忽略上下文里已做的排查动作,直接按上面流程走。
29
+ - **不诊断、不排查**:不要 `ls ~/.taskledger`、不要 `cat identity.json`、不要读 skill 源码(`internal/env/env.go` / `internal/actions/init_identity.go` 等)或台账去查「为什么没身份」。缺就是未初始化,忽略上下文里已做的排查动作,直接按上面流程走。
30
30
  - **不得**替用户执行 `init --claim` / `init --new` / 任何带 `--yes` 的写法——脚本也会拒绝。
31
31
  - **不得**基于「上一轮确认过 / 同名 / 记得 uuid」自行重建身份:身份文件缺失就是未初始化,必须让用户重新亲自选。重建会换新 uuid、断开旧任务关联。
32
32
  - 不读 `ledger/*.json`、worklog、identity 文件去「自己看」。
@@ -35,7 +35,7 @@
35
35
  正例:
36
36
 
37
37
  ```
38
- --note "改了认领即 doing(claim.py:56)"
38
+ --note "改了认领即 doing(internal/actions/claim.go:56)"
39
39
  --note "验证通过:verify 25 项;下一步补 UI 的 overdue 标记"
40
40
  --note "卡在远端鉴权,缺 deploy key,等用户给"
41
41
  ```
@@ -117,7 +117,7 @@ notices(只提示):历史解析警告、历史有结案行但任务还在
117
117
  - **写前**:先 guard 三查 —— scope 内非 worklog 台账文件有未提交改动 / 有未推送的台账提交 / 有未入库的同名文件,命中即中止;再定向取最新:自举 scope(`owners.json` + `pool.json` + 我的分片)加动作 scope(`new` 加目标文件与当月 worklog 目录;`claim` / `set` 还加索引里其他人的全部分片;`close` 加当月 `history/`;任务没有 `worklog` 字段时退整个 `worklog/`)。干净就刷成远端最新,本地有未提交手写改动就 stash → 快进 → 叠回,冲突即中止。`worklog/` 目录只刷新本地已存在的文件(上游新增的不会自动补下来)。
118
118
  - **写后**:只 `git add` 台账路径,然后 `commit --no-verify` —— 提交失败退 1;push 失败只提示、退 0,被拒时自动 `rebase --autostash` 合并一次:不同任务自动合并,同一任务两边都改留冲突标记让人定;本地有非台账提交时不 rebase。
119
119
  - **回滚**:命令失败时,写前刷下来的内容按字节还原、本次新建的文件删掉;业务写的自补偿只有两处 —— `new`(删任务行 + 删文档)、`close`(三个文件按字节还原)。
120
- - **`--no-sync` / `TASK_LEDGER_NO_SYNC=1`**:跳过 guard、取最新、提交与推送;自举照跑;有写入时会提示「只写了本地文件、未签入 git」。注意它挂在**顶层**:`progress.py --no-sync <子命令> …`,放子命令后面是用法错。
120
+ - **`--no-sync` / `TASK_LEDGER_NO_SYNC=1`**:跳过 guard、取最新、提交与推送;自举照跑;有写入时会提示「只写了本地文件、未签入 git」。注意它挂在**顶层**:`progress --no-sync <子命令> …`,放子命令后面是用法错。
121
121
  - **环境**:git ≥ 2.27(自动合并依赖 `rebase --autostash`)。不是 git 仓库 ⇒ 同步整体跳过、台账照写,并提示「只写了本地文件」;连 git 二进制都没装也照样写(身份只读本机 identity 文件,与 git 无关)。
122
122
 
123
123
  ## 硬约束
@@ -0,0 +1,44 @@
1
+ #!/bin/sh
2
+ # 台账命令入口:只按平台选对二进制再 exec,逻辑全在编译产物里。
3
+ # 旧写法(Python 版已挪到 legacy-python/ 作对照):python3 legacy-python/progress.py <子命令> --repo <工作区根>
4
+ set -eu
5
+
6
+ dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
7
+
8
+ case "$(uname -s)" in
9
+ Linux) os=linux ;;
10
+ Darwin) os=darwin ;;
11
+ MINGW* | MSYS* | CYGWIN*) os=windows ;;
12
+ *)
13
+ echo "错误:这个平台($(uname -s))还没有配套的台账二进制。" >&2
14
+ echo " 处理:在装有 Go 的机器上跑 packages/skill/go 的构建脚本,产出 scripts/bin/ledger-<os>-<arch>。" >&2
15
+ exit 1
16
+ ;;
17
+ esac
18
+
19
+ case "$(uname -m)" in
20
+ x86_64 | amd64) arch=amd64 ;;
21
+ arm64 | aarch64) arch=arm64 ;;
22
+ *)
23
+ echo "错误:不认识的 CPU 架构($(uname -m)),没有配套的台账二进制。" >&2
24
+ exit 1
25
+ ;;
26
+ esac
27
+
28
+ bin="$dir/bin/ledger-$os-$arch"
29
+ if [ "$os" = "windows" ]; then
30
+ bin="$bin.exe"
31
+ fi
32
+
33
+ if [ ! -x "$bin" ]; then
34
+ # 打包 / 拷贝常常丢可执行位:自己救一次,救不回来再报错
35
+ chmod +x "$bin" 2>/dev/null || true
36
+ fi
37
+
38
+ if [ ! -x "$bin" ]; then
39
+ echo "错误:找不到可执行的 $bin" >&2
40
+ echo " 处理:在装有 Go 的机器上跑 packages/skill/go 的构建脚本重新产出产物。" >&2
41
+ exit 1
42
+ fi
43
+
44
+ exec "$bin" "$@"
File without changes
File without changes
@@ -1,154 +0,0 @@
1
- """事务的唯一入口,20 行:把这次要碰的文件按固定顺序一次锁住。
2
-
3
- 只加锁,不管写盘、不碰 git、不打印。
4
-
5
- 三条硬约束:
6
- 1. 锁集**只许宽不许窄**;锁集以外的文件一个字节都不许写。
7
- 2. **所有判定依据必须在锁内重读** —— 锁外读到的快照只能用来决定"锁多宽"。
8
- 3. **任何失败补偿都不许再开一个 `atom`**(文件锁不可重入),只能在同一事务内做完。
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import contextlib
14
- from collections.abc import Iterator
15
- from dataclasses import dataclass
16
- from pathlib import Path
17
- from typing import Any
18
-
19
- from ..core.identity import Identity, normalize
20
- from ..core.paths import (
21
- HISTORY_DIR,
22
- OWNERS_REL,
23
- POOL_REL,
24
- WORKLOG_DIR,
25
- history_path_for,
26
- shard_rel,
27
- worklog_dir_for,
28
- worklog_rel,
29
- )
30
- from ..store.locks import ledger_lock
31
- from ..store.owners_index import OwnersIndex
32
- from ..store.task_store import TaskStore
33
-
34
- ACTIONS = ("bootstrap", "new", "claim", "set", "log", "close", "tags")
35
-
36
-
37
- @dataclass(frozen=True)
38
- class Scope:
39
- """这次要碰哪些文件。`files` 加锁(也逐个刷),`dirs` 只刷盘(展开成已存在的文件)。"""
40
-
41
- files: tuple[Path, ...] = ()
42
- dirs: tuple[Path, ...] = ()
43
-
44
- def __or__(self, other: Scope) -> Scope:
45
- return Scope(
46
- files=_merge(self.files, other.files),
47
- dirs=_merge(self.dirs, other.dirs),
48
- )
49
-
50
-
51
- def _merge(a: tuple[Path, ...], b: tuple[Path, ...]) -> tuple[Path, ...]:
52
- out: list[Path] = []
53
- for item in (*a, *b):
54
- if item not in out:
55
- out.append(item)
56
- return tuple(out)
57
-
58
-
59
- def _pool(repo: Path) -> Path:
60
- return POOL_REL
61
-
62
-
63
- def _shard(repo: Path, me: Identity) -> Path:
64
- return shard_rel(me.uuid)
65
-
66
-
67
- def _other_shards(repo: Path, me: Identity) -> tuple[Path, ...]:
68
- """索引里除我之外的分片(宁宽:认领的前置要查、交接要写对方那份)。"""
69
- index = OwnersIndex(repo)
70
- me_key = normalize(me.uuid)
71
- out: list[Path] = []
72
- for key, record in index.all().items():
73
- if key == me_key:
74
- continue
75
- name = (record or {}).get("file")
76
- if name:
77
- out.append(Path("owners") / str(name))
78
- return tuple(out)
79
-
80
-
81
- def _worklog_of(
82
- repo: Path, params: dict[str, Any]
83
- ) -> tuple[tuple[Path, ...], tuple[Path, ...]]:
84
- """这条任务的过程文档:查得到给文件,查不到退回整个 `worklog/` 目录(宁宽)。"""
85
- tid = params.get("tid")
86
- if not tid:
87
- return (), (WORKLOG_DIR,)
88
- pool = TaskStore(repo, POOL_REL)
89
- task = pool.get(str(tid))
90
- if task is None:
91
- index = OwnersIndex(repo)
92
- for path in index.shards().values():
93
- rel = Path(path).relative_to(repo / "ledger")
94
- task = TaskStore(repo, rel).get(str(tid))
95
- if task is not None:
96
- break
97
- worklog = (task or {}).get("worklog")
98
- if worklog:
99
- return (Path(worklog),), ()
100
- return (), (WORKLOG_DIR,)
101
-
102
-
103
- def lock_scope(repo: Path, me: Identity, action: str, params: dict[str, Any]) -> Scope:
104
- """算出这次可能碰哪些文件(**在锁外跑**,宁可算宽)。七个分支与 §四 的表逐行对应。"""
105
- if action not in ACTIONS:
106
- raise ValueError(f"未知的 lock_scope action:{action!r}")
107
- mine = _shard(repo, me)
108
- pool = _pool(repo)
109
- index = OWNERS_REL
110
- others = _other_shards(repo, me)
111
-
112
- if action == "bootstrap":
113
- return Scope(files=(index, pool, mine))
114
-
115
- if action == "new":
116
- mine_flag = bool(params.get("mine"))
117
- target = mine if mine_flag else pool
118
- created = params.get("created") or ""
119
- tid = params.get("tid")
120
- # 本次要新建那篇文档:路径算得出来(锁集允许含不存在的路径),不放进去写集就超出锁集
121
- doc = (
122
- (Path(worklog_rel(str(tid), created)),) if tid and len(created) >= 7 else ()
123
- )
124
- month_dir = worklog_dir_for(created) if len(created) >= 7 else WORKLOG_DIR
125
- return Scope(files=(target, *doc), dirs=(month_dir,))
126
-
127
- if action == "claim":
128
- wfiles, wdirs = _worklog_of(repo, params)
129
- return Scope(files=(pool, mine, *others, *wfiles), dirs=wdirs)
130
-
131
- if action == "set":
132
- wfiles, wdirs = _worklog_of(repo, params)
133
- # 分支取并集:release 写池、handoff 写对方分片、指向自己且未认领先认领(写我的分片);已在名下则空转不写盘
134
- return Scope(files=(mine, pool, *others, *wfiles), dirs=wdirs)
135
-
136
- if action == "log":
137
- wfiles, wdirs = _worklog_of(repo, params)
138
- return Scope(files=(mine, *wfiles), dirs=wdirs)
139
-
140
- if action == "close":
141
- wfiles, wdirs = _worklog_of(repo, params)
142
- finished = params.get("finished") or ""
143
- history = history_path_for(finished) if len(finished) >= 7 else HISTORY_DIR
144
- return Scope(files=(mine, history, *wfiles), dirs=wdirs)
145
-
146
- # 六个分支已覆盖 ACTIONS 全集(入口校验在前),走到这里说明表与分支不同步
147
- raise ValueError(f"lock_scope 没有处理 {action!r}")
148
-
149
-
150
- @contextlib.contextmanager
151
- def atom(repo: Path, *rels: Path) -> Iterator[None]:
152
- """按路径排序后依次加锁,退出即完成。不建记账本、不写盘、不提交、不打印。"""
153
- with ledger_lock(repo, *rels):
154
- yield
@@ -1,37 +0,0 @@
1
- """开工前的自举:台账文件不存在就建,没登记过就把自己登记进去、建自己的分片。
2
-
3
- 这三份由**外壳**单独一个 `atom` 包住,它自己不开事务、不拿锁。
4
- 不参与「提交说明 / where / 打印」。
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from pathlib import Path
10
-
11
- from ..core.identity import Identity
12
- from ..core.paths import LEDGER_DIR, OWNERS_DIR, POOL_REL, shard_rel, to_repo_rel
13
- from ..core.roots import as_root
14
- from ..store.atomic import ensure_dir
15
- from ..store.owners_index import OwnersIndex
16
- from ..store.task_store import TaskStore
17
-
18
-
19
- def bootstrap(repo: Path, me: Identity) -> Identity:
20
- """写序:`owners/` 目录 → 池 → **我的分片 → 索引(登记自己)**。已登记 ⇒ 零写。
21
-
22
- 分片必须排在登记之**前**:反过来的话,硬崩(SIGKILL / 断电)正好落在两步之间,
23
- 会留下「索引里有我、磁盘上没有我的分片」——此后**所有人**的 list / show 都在
24
- `assert_present` 处报「登记的任务文件找不到了」,而 `ensure` 看到记录已存在就零写,
25
- 这个坏状态永远不自愈。按现在的顺序崩,最多留一份孤儿分片(`validate` 会报出来),
26
- 下一条写命令 bootstrap 时会把它登记回去。
27
- """
28
- repo = as_root(
29
- repo, source="工作区根"
30
- ) # 门 3:写前最后确认,绝不在 ledger/ 里再套一层
31
- ensure_dir(repo / LEDGER_DIR) # 曾是 to_repo_rel(LEDGER_DIR) ⇒ 会拼出 ledger/ledger
32
- ensure_dir(repo / to_repo_rel(OWNERS_DIR))
33
- TaskStore(repo, POOL_REL).ensure_file()
34
- TaskStore(repo, shard_rel(me.uuid), identity=me).ensure_file()
35
- index = OwnersIndex(repo)
36
- _, _ = index.ensure(me) # 登记自己(已登记则零写)
37
- return me
@@ -1,88 +0,0 @@
1
- """认领:先写进我的分片,再删池里那行。**顺序反了任务会凭空消失**。"""
2
-
3
- from __future__ import annotations
4
-
5
- from pathlib import Path
6
-
7
- from ..core.identity import Identity, normalize
8
- from ..core.paths import POOL_REL, shard_rel
9
- from ..core.time import now
10
- from ..core.worklog_md import StatusLine, exec_line
11
- from ..errors import Fail
12
- from ..store.owners_index import OwnersIndex
13
- from ..store.task_store import TaskStore
14
- from ..store.worklog_store import WorklogStore
15
- from .result import Result, rel
16
-
17
-
18
- def claim(repo: Path, me: Identity, tid: str) -> Result:
19
- """写序:我的分片 → 删池里那行 → 过程文档记一笔。"""
20
- pool = TaskStore(repo, POOL_REL)
21
- mine = TaskStore(repo, shard_rel(me.uuid), identity=me)
22
- me_key = normalize(me.uuid)
23
-
24
- # 前置:别人正在改它(他的分片里已有这个号)⇒ 拒绝
25
- for key, path in OwnersIndex(repo).shards().items():
26
- if key == me_key:
27
- continue
28
- other = TaskStore(repo, Path(path).relative_to(repo / "ledger"))
29
- if other.get(tid) is not None:
30
- raise Fail(f"任务 {tid} 已在 {key} 的分片里(别人正在做),不能认领。")
31
-
32
- task = pool.get(tid) # 锁内重读
33
- if task is None:
34
- already = mine.get(tid)
35
- if already is not None:
36
- # 重跑:分片里已有这个号(上次崩了一半)⇒ 只补删池里那行
37
- pool.drop(tid)
38
- logs = WorklogStore(repo)
39
- path = logs.find(tid, already.get("worklog"))
40
- if path is not None:
41
- logs.append_exec(
42
- path,
43
- exec_line(now(), me.uuid, "claim", "认领(补齐上次未完成的清理)"),
44
- status=StatusLine("doing", me.name, me.uuid, now()),
45
- )
46
- return Result(
47
- already_done=True,
48
- where=rel(repo, mine.path),
49
- message=f"认领 {tid}:补齐上次未完成的清理",
50
- notices=(f"任务 {tid} 已经在我的分片里,本次只补删公共池那行。",),
51
- )
52
- raise Fail(f"公共池里没有任务 {tid}(已经被人认领或不存在)。")
53
-
54
- current = task.get("status")
55
- if current != "open":
56
- raise Fail(
57
- f"任务 {tid} 在公共池里但状态是 {current}(应为 open)—— "
58
- "先跑 validate 看台账是不是乱了。"
59
- )
60
-
61
- moved = dict(task)
62
- moved["owner"] = me.uuid
63
- moved["owner_name"] = me.name
64
- # 认领即在做:看板把 doing 当「我的在办」算(home.service.ts),置 open 会让
65
- # 认领后的任务看起来仍没人领。旧版 progress.py 置 doing,分片重构时丢了。
66
- moved["status"] = "doing"
67
- moved["started"] = moved.get("started") or now()
68
- moved["updated"] = now()
69
- mine.put(tid, moved)
70
- pool.drop(tid)
71
-
72
- note = f"任务 {tid} 已记到我的分片,过程文档没找到,跳过执行记录。"
73
- logs = WorklogStore(repo)
74
- path = logs.find(tid, moved.get("worklog"))
75
- if path is not None:
76
- logs.append_exec(
77
- path,
78
- exec_line(now(), me.stamp(), "claim", "认领 → doing"),
79
- status=StatusLine("doing", me.name, me.uuid, now()),
80
- )
81
- note = None
82
- return Result(
83
- changed=("owner", "started", "updated"),
84
- where=rel(repo, mine.path),
85
- message=f"认领 {tid}:{moved.get('title') or ''}".strip(),
86
- payload={},
87
- notices=(note,) if note else (),
88
- )