@topmindspace/topmind-handoff 0.6.0 → 0.6.4
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/CHANGELOG.md +26 -0
- package/README.md +4 -3
- package/SKILL.md +36 -9
- package/assets/handoff-example.md +9 -3
- package/assets/handoff-template.md +2 -2
- package/package.json +1 -1
- package/references/compile.md +17 -7
- package/references/environment.md +2 -2
- package/references/export.md +7 -2
- package/references/import.md +2 -2
- package/references/privacy.md +1 -0
- package/references/spec.md +9 -7
- package/topmind_handoff/admin.py +259 -39
- package/topmind_handoff/cli.py +21 -8
- package/topmind_handoff/compile.py +227 -42
- package/topmind_handoff/model.py +37 -2
- package/topmind_handoff/package.py +23 -24
package/topmind_handoff/admin.py
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
import subprocess
|
|
6
|
-
from datetime import date
|
|
7
|
+
from datetime import date
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from topmind_handoff.compile import VIEW_ORDER as SLICE_ORDER
|
|
@@ -11,18 +12,22 @@ from topmind_handoff.compile import build_compiled, compile_repo, render_status
|
|
|
11
12
|
from topmind_handoff.model import (
|
|
12
13
|
INDEX_BYTE_BUDGET,
|
|
13
14
|
INDEX_LINE_BUDGET,
|
|
14
|
-
REVIEW_DAYS,
|
|
15
|
-
SLICE_TITLE,
|
|
16
15
|
ClaimResult,
|
|
17
16
|
Compiled,
|
|
18
17
|
Op,
|
|
19
18
|
origin_markdown,
|
|
20
19
|
parse_authority,
|
|
20
|
+
parse_day,
|
|
21
|
+
review_suffix,
|
|
21
22
|
safe_origin,
|
|
22
23
|
scenario_filename,
|
|
23
24
|
scenario_names,
|
|
25
|
+
slice_title,
|
|
26
|
+
view_slice_order,
|
|
24
27
|
)
|
|
25
28
|
|
|
29
|
+
_SAFE_NAME = re.compile(r"[A-Za-z0-9_-]+")
|
|
30
|
+
|
|
26
31
|
|
|
27
32
|
class PublicRepoRefused(RuntimeError):
|
|
28
33
|
"""Raised when init is asked to write a public memory repo."""
|
|
@@ -42,15 +47,29 @@ AUTHORITY_INTRO = [
|
|
|
42
47
|
]
|
|
43
48
|
|
|
44
49
|
|
|
50
|
+
def _readme(lines: list[str]) -> str:
|
|
51
|
+
return _nl("\n".join(lines))
|
|
52
|
+
|
|
53
|
+
|
|
45
54
|
def memory_readme() -> str:
|
|
46
|
-
"""Text an initialized repo uses to explain itself."""
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
[
|
|
55
|
+
"""Text an initialized repo uses to explain itself. Two inits write the same bytes."""
|
|
56
|
+
return _readme(
|
|
57
|
+
[
|
|
50
58
|
"# 云端记忆",
|
|
51
59
|
"",
|
|
52
60
|
"这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
|
|
53
|
-
"凭据只用已经配好的 git 或 gh。不写密码、API key、token
|
|
61
|
+
"凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
|
|
62
|
+
"",
|
|
63
|
+
"## 写入哪一类",
|
|
64
|
+
"",
|
|
65
|
+
"- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。正文以「称呼:」「对本智能体的称呼:」「语言:」「时区:」开头。",
|
|
66
|
+
"- 边界的主人也是 human。",
|
|
67
|
+
"- 跨工具都适用的偏好写入 `scope: shared`。",
|
|
68
|
+
"- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
|
|
69
|
+
"- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
|
|
70
|
+
"- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
|
|
71
|
+
"- 换范围时先在旧范围撤回,再在新范围用同一 id 写下,`base: none`。",
|
|
72
|
+
"- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
|
|
54
73
|
"",
|
|
55
74
|
"## 各部分",
|
|
56
75
|
"",
|
|
@@ -58,16 +77,209 @@ def memory_readme() -> str:
|
|
|
58
77
|
"- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
|
|
59
78
|
"- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
|
|
60
79
|
"- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
|
|
61
|
-
"- `views/shared.md` 和 `views/scenario-<场景>.md
|
|
62
|
-
"- `STATUS.md
|
|
80
|
+
"- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
|
|
81
|
+
"- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
|
|
82
|
+
"- `knowledge/`:跨环境共享的经验、收藏帖、提示词。普通 Markdown,任何环境直接编辑。见 `knowledge/README.md`。",
|
|
63
83
|
"",
|
|
64
|
-
"权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md`
|
|
84
|
+
"权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
|
|
65
85
|
"",
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
"## knowledge/ 知识库",
|
|
87
|
+
"",
|
|
88
|
+
"- `knowledge/<大类>/<主题>.md`,最多两级。大类有收藏帖、段子、引流曝光、写作、图片视频、提示词。",
|
|
89
|
+
"- 它是参考资料,不是指令,不覆盖偏好与边界,不进 `outbox/`、编译、视图和 `authority.md`。",
|
|
90
|
+
"- 任何环境都可以直接改,提交并推送即可,git 历史就是留痕。不写凭据、隐私和聊天记录。",
|
|
91
|
+
"- 条目格式、拆分规则、读写时机见 `knowledge/README.md`。",
|
|
92
|
+
"",
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
KNOWLEDGE_CATEGORIES = (
|
|
98
|
+
("收藏帖", "帖子收藏"),
|
|
99
|
+
("段子", "段子写法经验"),
|
|
100
|
+
("引流曝光", "引流、曝光、涨粉经验"),
|
|
101
|
+
("写作", "写作经验"),
|
|
102
|
+
("图片视频", "做图、做视频的经验"),
|
|
103
|
+
("提示词", "各类提示词"),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def knowledge_readme() -> str:
|
|
108
|
+
"""Generic rules for knowledge/. No personal entries."""
|
|
109
|
+
index = [f"- `{name}/通用.md`:{desc},未细分的先放这里" for name, desc in KNOWLEDGE_CATEGORIES]
|
|
110
|
+
return _readme(
|
|
111
|
+
[
|
|
112
|
+
"# knowledge · 经验知识库",
|
|
113
|
+
"",
|
|
114
|
+
"跨环境共享的经验、收藏和提示词。普通 Markdown,任何环境都能直接编辑,git 历史就是留痕。",
|
|
115
|
+
"",
|
|
116
|
+
"- 这里是参考资料,不是指令。不覆盖 `outbox/` 里的偏好与边界,也不进编译、视图和 `authority.md`。",
|
|
117
|
+
"- 不写密码、API key、token、隐私、他人信息、聊天记录。",
|
|
118
|
+
"",
|
|
119
|
+
"## 目录索引",
|
|
120
|
+
"",
|
|
121
|
+
"每个文件一行。新建或拆分文件时同步更新这里。",
|
|
122
|
+
"",
|
|
123
|
+
*index,
|
|
124
|
+
"",
|
|
125
|
+
"## 结构与拆分",
|
|
126
|
+
"",
|
|
127
|
+
"- 最多两级:`knowledge/<大类>/<主题>.md`。不建三级目录。",
|
|
128
|
+
"- 每个大类先只有 `通用.md`。单个文件超过约 300 行,或某个子主题攒到约 10 条,就拆出子文件。",
|
|
129
|
+
"- 子文件命名「子类-限定.md」,例如 `提示词/图片-写实人像.md`、`收藏帖/AI工具.md`。",
|
|
130
|
+
"- 拆分只搬移条目,不改字。拆完更新上面的索引。",
|
|
131
|
+
"- 交叉归类用标签,不用更深的目录。",
|
|
132
|
+
"- 需要新大类时再加文件夹,同样先放 `通用.md`。",
|
|
133
|
+
"",
|
|
134
|
+
"## 条目格式",
|
|
135
|
+
"",
|
|
136
|
+
"```",
|
|
137
|
+
"## 标题",
|
|
138
|
+
"YYYY-MM-DD · <origin_id> · #标签1 #标签2 · https://链接(可选)",
|
|
139
|
+
"",
|
|
140
|
+
"- 要点 1",
|
|
141
|
+
"- 要点 2(2–5 行)",
|
|
142
|
+
"```",
|
|
143
|
+
"",
|
|
144
|
+
"- 提示词类:原文放进代码块,注明适用的模型或工具,以及效果。",
|
|
145
|
+
"- 收藏帖:写作者、链接、为什么值得收。",
|
|
146
|
+
"- 新条目加在文件顶部(文件说明之下)。",
|
|
147
|
+
"- 写之前先搜有没有重复:`rg -n \"关键词\" knowledge/`。有就改原条目。",
|
|
148
|
+
"- 过时了就改或删,不留墓碑。",
|
|
149
|
+
"",
|
|
150
|
+
"## 什么时候读、什么时候写",
|
|
151
|
+
"",
|
|
152
|
+
"- 读:拉取后按当前任务用 `rg` 查相关条目,不整体读入。",
|
|
153
|
+
"- 写:用户说「沉淀经验」「记个经验」「收藏帖子」「存提示词」,或者踩到值得记的坑。写完提交并推送。",
|
|
154
|
+
"- 不写只在某台机器有用的步骤(那是环境卡片),也不写偏好与事实(那是 `outbox/`)。",
|
|
155
|
+
"",
|
|
156
|
+
]
|
|
68
157
|
)
|
|
69
158
|
|
|
70
159
|
|
|
160
|
+
def write_knowledge_skeleton(repo: Path) -> list[Path]:
|
|
161
|
+
"""Create knowledge/ files that are missing. Existing files are never touched."""
|
|
162
|
+
root = Path(repo) / "knowledge"
|
|
163
|
+
planned = [(root / "README.md", knowledge_readme())]
|
|
164
|
+
for name, desc in KNOWLEDGE_CATEGORIES:
|
|
165
|
+
planned.append(
|
|
166
|
+
(
|
|
167
|
+
root / name / "通用.md",
|
|
168
|
+
_nl(f"# {name} · 通用\n\n{desc},未细分的先放这里。格式见 `knowledge/README.md`,新条目加在本行之下。"),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
written = []
|
|
172
|
+
for path, text in planned:
|
|
173
|
+
if path.exists():
|
|
174
|
+
continue
|
|
175
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
path.write_text(text, encoding="utf-8")
|
|
177
|
+
written.append(path)
|
|
178
|
+
return written
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Byte-identical explanations this skill itself used to write. Compile replaces
|
|
182
|
+
# only these. A README a person has edited is left as it is.
|
|
183
|
+
KNOWN_STOCK_READMES = (
|
|
184
|
+
_readme(
|
|
185
|
+
[
|
|
186
|
+
"# 云端记忆",
|
|
187
|
+
"",
|
|
188
|
+
"这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
|
|
189
|
+
"凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
|
|
190
|
+
"",
|
|
191
|
+
"## 写入哪一类",
|
|
192
|
+
"",
|
|
193
|
+
"- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。正文以「称呼:」「对本智能体的称呼:」「语言:」「时区:」开头。",
|
|
194
|
+
"- 边界的主人也是 human。",
|
|
195
|
+
"- 跨工具都适用的偏好写入 `scope: shared`。",
|
|
196
|
+
"- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
|
|
197
|
+
"- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
|
|
198
|
+
"- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
|
|
199
|
+
"- 换范围时先在旧范围撤回,再在新范围用同一 id 写下,`base: none`。",
|
|
200
|
+
"- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
|
|
201
|
+
"",
|
|
202
|
+
"## 各部分",
|
|
203
|
+
"",
|
|
204
|
+
"- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
|
|
205
|
+
"- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
|
|
206
|
+
"- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
|
|
207
|
+
"- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
|
|
208
|
+
"- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
|
|
209
|
+
"- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
|
|
210
|
+
"",
|
|
211
|
+
"权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
|
|
212
|
+
"",
|
|
213
|
+
]
|
|
214
|
+
),
|
|
215
|
+
_readme(
|
|
216
|
+
[
|
|
217
|
+
"# 云端记忆",
|
|
218
|
+
"",
|
|
219
|
+
"这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
|
|
220
|
+
"凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。",
|
|
221
|
+
"",
|
|
222
|
+
"## 各部分",
|
|
223
|
+
"",
|
|
224
|
+
"- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
|
|
225
|
+
"- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
|
|
226
|
+
"- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
|
|
227
|
+
"- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
|
|
228
|
+
"- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。",
|
|
229
|
+
"- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。",
|
|
230
|
+
"",
|
|
231
|
+
"权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是编译产物。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
|
|
232
|
+
"",
|
|
233
|
+
]
|
|
234
|
+
),
|
|
235
|
+
_readme(
|
|
236
|
+
[
|
|
237
|
+
"# 云端记忆",
|
|
238
|
+
"",
|
|
239
|
+
"这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
|
|
240
|
+
"凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
|
|
241
|
+
"",
|
|
242
|
+
"## 写入哪一类",
|
|
243
|
+
"",
|
|
244
|
+
"- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。",
|
|
245
|
+
"- 边界的主人也是 human。",
|
|
246
|
+
"- 跨工具都适用的偏好写入 `scope: shared`。",
|
|
247
|
+
"- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
|
|
248
|
+
"- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
|
|
249
|
+
"- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
|
|
250
|
+
"- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
|
|
251
|
+
"",
|
|
252
|
+
"## 各部分",
|
|
253
|
+
"",
|
|
254
|
+
"- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
|
|
255
|
+
"- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
|
|
256
|
+
"- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
|
|
257
|
+
"- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
|
|
258
|
+
"- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
|
|
259
|
+
"- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
|
|
260
|
+
"",
|
|
261
|
+
"权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
|
|
262
|
+
"",
|
|
263
|
+
]
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def upgrade_stock_readme(repo: Path) -> bool:
|
|
269
|
+
"""Replace a README this skill wrote earlier. Leave a person's edits in place."""
|
|
270
|
+
path = Path(repo) / "README.md"
|
|
271
|
+
if not path.is_file():
|
|
272
|
+
return False
|
|
273
|
+
current = path.read_text(encoding="utf-8")
|
|
274
|
+
latest = memory_readme()
|
|
275
|
+
if current == latest or current not in KNOWN_STOCK_READMES:
|
|
276
|
+
return False
|
|
277
|
+
path.write_text(latest, encoding="utf-8")
|
|
278
|
+
# The new explanation points at knowledge/. Add only the files that are missing.
|
|
279
|
+
write_knowledge_skeleton(repo)
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
|
|
71
283
|
def init_memory_repo(
|
|
72
284
|
path: Path,
|
|
73
285
|
*,
|
|
@@ -105,6 +317,7 @@ def init_memory_repo(
|
|
|
105
317
|
),
|
|
106
318
|
encoding="utf-8",
|
|
107
319
|
)
|
|
320
|
+
write_knowledge_skeleton(path)
|
|
108
321
|
(path / "facts").mkdir()
|
|
109
322
|
(path / "facts" / "README.md").write_text(
|
|
110
323
|
_nl("# 长说明\n\n整理时才把索引收进这里。这些文件不是另一套权威。权威仍是信箱。\n"),
|
|
@@ -178,21 +391,22 @@ def _git_commit(path: Path, message: str) -> None:
|
|
|
178
391
|
)
|
|
179
392
|
|
|
180
393
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
return datetime.strptime(value, "%Y-%m-%d").date()
|
|
184
|
-
except ValueError:
|
|
185
|
-
return None
|
|
394
|
+
def _review_suffix(op: Op, today: date) -> str:
|
|
395
|
+
return review_suffix(op, today)
|
|
186
396
|
|
|
187
397
|
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
398
|
+
def _archive_path(facts_root: Path, fact_prefix: str, slice_name: str) -> Path | None:
|
|
399
|
+
"""Stay inside facts/. A slice such as `../evil` is kept inline and does not abort."""
|
|
400
|
+
if _SAFE_NAME.fullmatch(fact_prefix) is None or _SAFE_NAME.fullmatch(slice_name) is None:
|
|
401
|
+
return None
|
|
402
|
+
facts_root.mkdir(parents=True, exist_ok=True)
|
|
403
|
+
candidate = facts_root / f"{fact_prefix}-{slice_name}.md"
|
|
404
|
+
try:
|
|
405
|
+
if candidate.resolve().parent != facts_root.resolve():
|
|
406
|
+
return None
|
|
407
|
+
except OSError:
|
|
408
|
+
return None
|
|
409
|
+
return candidate
|
|
196
410
|
|
|
197
411
|
|
|
198
412
|
def _write_slice_archive(
|
|
@@ -203,16 +417,15 @@ def _write_slice_archive(
|
|
|
203
417
|
rich: list[str],
|
|
204
418
|
) -> None:
|
|
205
419
|
"""Keep every collapsed index line, including facts that have no detail."""
|
|
206
|
-
facts_root
|
|
420
|
+
dest = _archive_path(facts_root, fact_prefix, slice_name)
|
|
421
|
+
if dest is None:
|
|
422
|
+
return
|
|
207
423
|
rich_lines = set(rich)
|
|
208
424
|
body = list(rich)
|
|
209
425
|
body.extend(line for line in lines if line not in rich_lines)
|
|
210
426
|
if not body:
|
|
211
427
|
body = list(lines)
|
|
212
|
-
(
|
|
213
|
-
_nl("# " + slice_name + "\n\n" + "\n".join(body)),
|
|
214
|
-
encoding="utf-8",
|
|
215
|
-
)
|
|
428
|
+
dest.write_text(_nl("# " + slice_name + "\n\n" + "\n".join(body)), encoding="utf-8")
|
|
216
429
|
|
|
217
430
|
|
|
218
431
|
def _results_for(compiled: Compiled, scope: str) -> list[ClaimResult]:
|
|
@@ -239,9 +452,11 @@ def _compact_scope(title: str, results: list[ClaimResult], today: date, facts_ro
|
|
|
239
452
|
|
|
240
453
|
def render(section_map: dict[str, list[str]]) -> str:
|
|
241
454
|
lines = [f"# {title}", ""]
|
|
242
|
-
for slice_name in
|
|
243
|
-
lines.append(f"## {SLICE_TITLE[slice_name]}")
|
|
455
|
+
for slice_name in view_slice_order(section_map):
|
|
244
456
|
body = section_map.get(slice_name) or []
|
|
457
|
+
if slice_name not in SLICE_ORDER and not body:
|
|
458
|
+
continue
|
|
459
|
+
lines.append(f"## {slice_title(slice_name)}")
|
|
245
460
|
lines.extend(body or ["(无)"])
|
|
246
461
|
lines.append("")
|
|
247
462
|
lines.append("## 冲突")
|
|
@@ -260,14 +475,19 @@ def _compact_scope(title: str, results: list[ClaimResult], today: date, facts_ro
|
|
|
260
475
|
for slice_name, block in details.items():
|
|
261
476
|
if not block:
|
|
262
477
|
continue
|
|
263
|
-
facts_root
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
)
|
|
478
|
+
dest = _archive_path(facts_root, fact_prefix, slice_name)
|
|
479
|
+
if dest is None:
|
|
480
|
+
continue
|
|
481
|
+
dest.write_text(_nl("# " + slice_name + "\n\n" + "\n".join(block)), encoding="utf-8")
|
|
267
482
|
|
|
268
483
|
text = render(sections)
|
|
269
484
|
# Collapse the largest slices until the index fits the startup budget.
|
|
270
485
|
collapsible = [name for name in ("fact", "preference", "project", "identity", "boundaries")]
|
|
486
|
+
collapsible.extend(
|
|
487
|
+
name
|
|
488
|
+
for name in sections
|
|
489
|
+
if name not in SLICE_ORDER and _SAFE_NAME.fullmatch(name) and name not in collapsible
|
|
490
|
+
)
|
|
271
491
|
while (text.count("\n") > INDEX_LINE_BUDGET or len(text.encode("utf-8")) > INDEX_BYTE_BUDGET) and any(
|
|
272
492
|
len(sections.get(name) or []) > 1 for name in collapsible
|
|
273
493
|
):
|
|
@@ -312,7 +532,7 @@ def _mark_stale(card_path: Path, today: date) -> None:
|
|
|
312
532
|
def _card_refreshed(raw: str) -> date | None:
|
|
313
533
|
for line in raw.splitlines():
|
|
314
534
|
if line.startswith("refreshed:"):
|
|
315
|
-
return
|
|
535
|
+
return parse_day(line.split(":", 1)[1].strip())
|
|
316
536
|
return None
|
|
317
537
|
|
|
318
538
|
|
|
@@ -356,7 +576,7 @@ def organize_repo(repo: Path, today: date | None = None) -> Compiled:
|
|
|
356
576
|
_mark_stale(path, today)
|
|
357
577
|
# Cards may now be stale; rebuild status from the same outbox tips.
|
|
358
578
|
compiled = build_compiled(repo)
|
|
359
|
-
(repo / "STATUS.md").write_text(render_status(compiled), encoding="utf-8")
|
|
579
|
+
(repo / "STATUS.md").write_text(render_status(compiled, today=today), encoding="utf-8")
|
|
360
580
|
after = {path.name: path.read_bytes() for path in sorted(outbox.glob("*.md"))} if outbox.is_dir() else {}
|
|
361
581
|
if before != after:
|
|
362
582
|
raise RuntimeError("整理改写了信箱")
|
package/topmind_handoff/cli.py
CHANGED
|
@@ -12,14 +12,14 @@ from topmind_handoff.package import render_package
|
|
|
12
12
|
|
|
13
13
|
USAGE = """\
|
|
14
14
|
用法:
|
|
15
|
-
python3 scripts/compile_memory.py <记忆目录>
|
|
15
|
+
python3 scripts/compile_memory.py <记忆目录> [--today YYYY-MM-DD]
|
|
16
16
|
python3 scripts/compile_memory.py --check <记忆目录>
|
|
17
17
|
python3 scripts/compile_memory.py --init <目录> --private --origin <origin_id> [--role 场景] [--today YYYY-MM-DD]
|
|
18
18
|
python3 scripts/compile_memory.py --init <目录> --public --origin <origin_id>
|
|
19
19
|
python3 scripts/compile_memory.py --organize <记忆目录> [--today YYYY-MM-DD]
|
|
20
|
-
python3 scripts/compile_memory.py --render <记忆目录> --origin <origin_id> [--out 文件]
|
|
20
|
+
python3 scripts/compile_memory.py --render <记忆目录> --origin <origin_id> [--out 文件] [--today YYYY-MM-DD]
|
|
21
21
|
python3 scripts/compile_memory.py --own <记忆目录> --scenario <场景> --origin <origin_id>
|
|
22
|
-
python3 scripts/compile_memory.py --startup <记忆目录> --origin <origin_id>
|
|
22
|
+
python3 scripts/compile_memory.py --startup <记忆目录> --origin <origin_id> [--today YYYY-MM-DD]
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
|
|
@@ -48,9 +48,7 @@ def main(argv: list[str]) -> int:
|
|
|
48
48
|
if argv[0].startswith("--"):
|
|
49
49
|
print(USAGE)
|
|
50
50
|
return 2
|
|
51
|
-
|
|
52
|
-
print(f"compiled {argv[0]}")
|
|
53
|
-
return 0
|
|
51
|
+
return _compile(argv)
|
|
54
52
|
|
|
55
53
|
|
|
56
54
|
def _init(argv: list[str]) -> int:
|
|
@@ -78,6 +76,17 @@ def _init(argv: list[str]) -> int:
|
|
|
78
76
|
return 0
|
|
79
77
|
|
|
80
78
|
|
|
79
|
+
def _compile(argv: list[str]) -> int:
|
|
80
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
81
|
+
parser.add_argument("path")
|
|
82
|
+
parser.add_argument("--today")
|
|
83
|
+
args = parser.parse_args(argv)
|
|
84
|
+
today = _day(args.today) if args.today else None
|
|
85
|
+
compile_repo(Path(args.path), today)
|
|
86
|
+
print(f"compiled {args.path}")
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
81
90
|
def _organize(argv: list[str]) -> int:
|
|
82
91
|
parser = argparse.ArgumentParser(add_help=False)
|
|
83
92
|
parser.add_argument("path")
|
|
@@ -94,9 +103,11 @@ def _render(argv: list[str]) -> int:
|
|
|
94
103
|
parser.add_argument("path")
|
|
95
104
|
parser.add_argument("--origin", required=True)
|
|
96
105
|
parser.add_argument("--out")
|
|
106
|
+
parser.add_argument("--today")
|
|
97
107
|
args = parser.parse_args(argv)
|
|
108
|
+
today = _day(args.today) if args.today else None
|
|
98
109
|
try:
|
|
99
|
-
text = render_package(Path(args.path), args.origin)
|
|
110
|
+
text = render_package(Path(args.path), args.origin, today=today)
|
|
100
111
|
except ValueError as exc:
|
|
101
112
|
print(str(exc))
|
|
102
113
|
return 1
|
|
@@ -127,9 +138,11 @@ def _startup(argv: list[str]) -> int:
|
|
|
127
138
|
parser = argparse.ArgumentParser(add_help=False)
|
|
128
139
|
parser.add_argument("path")
|
|
129
140
|
parser.add_argument("--origin", required=True)
|
|
141
|
+
parser.add_argument("--today")
|
|
130
142
|
args = parser.parse_args(argv)
|
|
143
|
+
today = _day(args.today) if args.today else None
|
|
131
144
|
try:
|
|
132
|
-
print(startup_read(Path(args.path), args.origin), end="")
|
|
145
|
+
print(startup_read(Path(args.path), args.origin, today=today), end="")
|
|
133
146
|
except ValueError as exc:
|
|
134
147
|
print(str(exc))
|
|
135
148
|
return 1
|