task-ledger 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/references/state-model.md +1 -1
- package/scripts/ledgerkit/actions/bootstrap.py +5 -6
- package/scripts/ledgerkit/actions/guard.py +3 -7
- package/scripts/ledgerkit/actions/query_history.py +1 -2
- package/scripts/ledgerkit/core/identity.py +3 -19
- package/scripts/ledgerkit/env.py +1 -1
- package/scripts/ledgerkit/store/owners_index.py +2 -23
- package/scripts/ledgerkit/store/task_store.py +2 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-ledger",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
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",
|
|
@@ -35,7 +35,7 @@ close → 历史层 + 封卷 + 移出自己名下
|
|
|
35
35
|
|
|
36
36
|
- 主键 = **git 邮箱**(`git var GIT_COMMITTER_IDENT` 里的 email,退回 `git config user.email`),归一化(去空白 + 小写)后比对。
|
|
37
37
|
- 读不到邮箱**不报错**:走兜底链(`USER`/`LOGNAME` → `getpass` → 主机名 → 字面 `default`)并出一条提示,命令照常跑完。看到提示要告诉用户配好 git,否则任务记到兜底身份名下。
|
|
38
|
-
-
|
|
38
|
+
- 全站只认 git 邮箱这一个身份键;`list --owner` / `history --owner` 是归一化后的**精确匹配**,没有 `me` 缩写,「自己」用 `--mine`。
|
|
39
39
|
- `close` / `log` 只对自己名下的任务生效,不在名下直接拒绝;`set --owner ""` 对已在公共池里的任务是幂等空转(退 0);`set --owner <自己>` 会把公共池里的任务认领到自己名下。
|
|
40
40
|
|
|
41
41
|
## 历史层
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""开工前的自举:台账文件不存在就建,没登记过就把自己登记进去、建自己的分片。
|
|
2
2
|
|
|
3
3
|
这三份由**外壳**单独一个 `atom` 包住,它自己不开事务、不拿锁。
|
|
4
|
-
|
|
4
|
+
不参与「提交说明 / where / 打印」。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
from ..core.identity import Identity
|
|
11
|
+
from ..core.identity import Identity
|
|
12
12
|
from ..core.paths import LEDGER_DIR, OWNERS_DIR, POOL_REL, shard_rel, to_repo_rel
|
|
13
13
|
from ..store.atomic import ensure_dir
|
|
14
14
|
from ..store.owners_index import OwnersIndex
|
|
@@ -16,12 +16,11 @@ from ..store.task_store import TaskStore
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def bootstrap(repo: Path, me: Identity) -> Identity:
|
|
19
|
-
"""写序:`owners/` 目录 → 池 → 索引(登记自己)→
|
|
19
|
+
"""写序:`owners/` 目录 → 池 → 索引(登记自己)→ 我的分片。已登记 ⇒ 零写。"""
|
|
20
20
|
ensure_dir(repo / to_repo_rel(LEDGER_DIR))
|
|
21
21
|
ensure_dir(repo / to_repo_rel(OWNERS_DIR))
|
|
22
22
|
TaskStore(repo, POOL_REL).ensure_file()
|
|
23
23
|
index = OwnersIndex(repo)
|
|
24
|
-
_,
|
|
24
|
+
_, _ = index.ensure(me) # 登记自己(已登记则零写)
|
|
25
25
|
TaskStore(repo, shard_rel(me.email), identity=me).ensure_file()
|
|
26
|
-
|
|
27
|
-
return with_aliases(me, aliases) if aliases else me
|
|
26
|
+
return me
|
|
@@ -20,14 +20,10 @@ from .query import ListQuery
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def current_identity(repo: Path) -> Identity:
|
|
23
|
-
"""env
|
|
23
|
+
"""env 的本机身份(不写盘、不登记)。全站只认邮箱这一个身份键。"""
|
|
24
24
|
from .. import env
|
|
25
|
-
from ..core.identity import with_aliases
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
record = OwnersIndex(repo).get(me.email) or {}
|
|
29
|
-
aliases = tuple(record.get("aliases") or ())
|
|
30
|
-
return with_aliases(me, aliases) if aliases else me
|
|
26
|
+
return env.current(repo)
|
|
31
27
|
|
|
32
28
|
|
|
33
29
|
def require_task(tasks: Mapping[str, Any], tid: str, *, where: Path) -> dict[str, Any]:
|
|
@@ -72,7 +68,7 @@ def status_of(task: Mapping[str, Any]) -> str:
|
|
|
72
68
|
|
|
73
69
|
|
|
74
70
|
def is_mine(task: Mapping[str, Any], me: Identity) -> bool:
|
|
75
|
-
"""
|
|
71
|
+
"""这条任务是不是我的(按邮箱判定)。"""
|
|
76
72
|
return is_me(me, owner_of(task) or "")
|
|
77
73
|
|
|
78
74
|
|
|
@@ -127,8 +127,7 @@ def query_history(repo: Path, me: Identity, q: HistoryQuery) -> HistoryResult:
|
|
|
127
127
|
records.append(row)
|
|
128
128
|
|
|
129
129
|
if q.mine:
|
|
130
|
-
wanted = {
|
|
131
|
-
wanted = {normalize(k) for k in wanted}
|
|
130
|
+
wanted = {normalize(me.email)}
|
|
132
131
|
records = [r for r in records if normalize(r.get("owner") or "") in wanted]
|
|
133
132
|
elif q.owner:
|
|
134
133
|
key = normalize(q.owner)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import dataclass
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass(frozen=True)
|
|
@@ -11,7 +11,6 @@ class Identity:
|
|
|
11
11
|
|
|
12
12
|
email: str
|
|
13
13
|
name: str = ""
|
|
14
|
-
aliases: tuple[str, ...] = ()
|
|
15
14
|
|
|
16
15
|
def label(self) -> str:
|
|
17
16
|
"""显示名:没名字就用邮箱。"""
|
|
@@ -23,21 +22,6 @@ def normalize(who: str) -> str:
|
|
|
23
22
|
return (who or "").strip().lower()
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
def keys(identity: Identity) -> tuple[str, ...]:
|
|
27
|
-
"""这个身份的全部可比标识:自己 + aliases(去重、保持顺序)。"""
|
|
28
|
-
out: list[str] = []
|
|
29
|
-
for value in (identity.email, *identity.aliases):
|
|
30
|
-
key = normalize(value)
|
|
31
|
-
if key and key not in out:
|
|
32
|
-
out.append(key)
|
|
33
|
-
return tuple(out)
|
|
34
|
-
|
|
35
|
-
|
|
36
25
|
def is_me(identity: Identity, who: str | None) -> bool:
|
|
37
|
-
"""`who
|
|
38
|
-
return normalize(who or "")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def with_aliases(identity: Identity, aliases: tuple[str, ...]) -> Identity:
|
|
42
|
-
"""换一个带 aliases 的同身份对象(索引里读到别名后由外壳装配)。"""
|
|
43
|
-
return replace(identity, aliases=tuple(aliases))
|
|
26
|
+
"""`who`(邮箱)是不是这个人。全站只认邮箱这一个身份键。"""
|
|
27
|
+
return normalize(who or "") == normalize(identity.email)
|
package/scripts/ledgerkit/env.py
CHANGED
|
@@ -90,4 +90,4 @@ def current(repo: Path | None = None) -> Identity:
|
|
|
90
90
|
"配好 git config user.email 再跑,任务才会记到正确的名下。"
|
|
91
91
|
)
|
|
92
92
|
name = git_name(repo) or email
|
|
93
|
-
return Identity(email=normalize(email), name=name.strip()
|
|
93
|
+
return Identity(email=normalize(email), name=name.strip())
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""索引 `owners.json`:谁是登记过的人、各自的分片文件是哪个、新人的登记与改名。
|
|
2
2
|
|
|
3
3
|
**分片只认索引,不扫目录** —— 唯一例外是全仓仅此一处的 `disk_shards()`,它只给 `validate` 用。
|
|
4
|
-
`ensure` 是 `aliases` 的唯一写入点。
|
|
5
4
|
"""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -32,7 +31,7 @@ class OwnersIndex:
|
|
|
32
31
|
return self._store.all()
|
|
33
32
|
|
|
34
33
|
def ensure(self, me: Identity) -> tuple[bool, dict[str, Any]]:
|
|
35
|
-
"""登记 / 补 `joined`
|
|
34
|
+
"""登记 / 补 `joined`。`name` 只在首次登记时写入,之后不回写(名字是给你做对照的被动字段,不该被任何逻辑读)。返回 (这次写了没有, 记录)。"""
|
|
36
35
|
key = normalize(me.email)
|
|
37
36
|
with self._open() as session:
|
|
38
37
|
data = session.data
|
|
@@ -43,23 +42,11 @@ class OwnersIndex:
|
|
|
43
42
|
"file": shard_rel(key).name,
|
|
44
43
|
"name": me.name,
|
|
45
44
|
"joined": now(),
|
|
46
|
-
"aliases": _merge_aliases([], me.aliases),
|
|
47
45
|
}
|
|
48
46
|
workers[key] = record
|
|
49
47
|
session.mark()
|
|
50
48
|
return True, dict(record)
|
|
51
|
-
|
|
52
|
-
if me.name and record.get("name") != me.name:
|
|
53
|
-
record["name"] = me.name
|
|
54
|
-
changed = True
|
|
55
|
-
known = list(record.get("aliases") or [])
|
|
56
|
-
aliases = _merge_aliases(known, me.aliases) # 只增不删
|
|
57
|
-
if aliases != known:
|
|
58
|
-
record["aliases"] = aliases
|
|
59
|
-
changed = True
|
|
60
|
-
if changed:
|
|
61
|
-
session.mark()
|
|
62
|
-
return changed, dict(record)
|
|
49
|
+
return False, dict(record)
|
|
63
50
|
|
|
64
51
|
def shards(self) -> dict[str, Path]:
|
|
65
52
|
"""身份 → 分片绝对路径(只信索引)。"""
|
|
@@ -125,11 +112,3 @@ class _IndexSession:
|
|
|
125
112
|
self.store._flush(self.data) # 同一个包内的受控写回口
|
|
126
113
|
|
|
127
114
|
|
|
128
|
-
def _merge_aliases(known: list, extra: tuple[str, ...] | list[str]) -> list[str]:
|
|
129
|
-
"""把新别名并进去,只增不删、去重保序。"""
|
|
130
|
-
out = list(known)
|
|
131
|
-
for alias in extra or ():
|
|
132
|
-
key = normalize(alias)
|
|
133
|
-
if key and key not in out:
|
|
134
|
-
out.append(key)
|
|
135
|
-
return out
|
|
@@ -22,10 +22,7 @@ class TaskStore(JSONStore):
|
|
|
22
22
|
self.identity = identity
|
|
23
23
|
|
|
24
24
|
def _flush(self, data: dict[str, Any]) -> None:
|
|
25
|
-
"""写盘时带上分片顶部的 `identity
|
|
25
|
+
"""写盘时带上分片顶部的 `identity`(只存邮箱;显示名在索引里)。"""
|
|
26
26
|
if self.identity is not None:
|
|
27
|
-
data["identity"] = {
|
|
28
|
-
"email": self.identity.email,
|
|
29
|
-
"name": self.identity.name,
|
|
30
|
-
}
|
|
27
|
+
data["identity"] = {"email": self.identity.email}
|
|
31
28
|
super()._flush(data)
|