@topmindspace/topmind-handoff 0.5.2 → 0.6.3
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 +24 -0
- package/README.md +7 -6
- package/SKILL.md +52 -14
- package/assets/handoff-example.md +10 -4
- package/assets/handoff-template.md +3 -3
- package/package.json +1 -1
- package/references/compile.md +18 -7
- package/references/environment.md +2 -2
- package/references/export.md +7 -2
- package/references/import.md +5 -10
- package/references/privacy.md +1 -0
- package/references/spec.md +16 -17
- package/topmind_handoff/admin.py +168 -48
- package/topmind_handoff/cli.py +22 -28
- package/topmind_handoff/compile.py +251 -42
- package/topmind_handoff/model.py +37 -2
- package/topmind_handoff/package.py +57 -75
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
"""Render
|
|
1
|
+
"""Render one handoff package and replay its ledger. Same compiler, no network."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
from copy import deepcopy
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import date
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
|
-
from topmind_handoff.compile import compile_repo
|
|
11
|
-
from topmind_handoff.model import
|
|
11
|
+
from topmind_handoff.compile import append_outbox, compile_repo, projection_body
|
|
12
|
+
from topmind_handoff.model import (
|
|
13
|
+
REVIEW_MARK,
|
|
14
|
+
ROOT,
|
|
15
|
+
compiler_version,
|
|
16
|
+
origin_markdown,
|
|
17
|
+
parse_outbox,
|
|
18
|
+
safe_origin,
|
|
19
|
+
)
|
|
12
20
|
|
|
13
21
|
APPENDIX_MARK = "本附录是操作账本,不是新事实。按 op_id 去重重放;不要把这些条目再导入为新的事实或记忆。"
|
|
14
22
|
ANCHOR_FIELDS = ("对本智能体的称呼", "称呼", "语言", "时区")
|
|
23
|
+
PACKAGE_VERSION = "2"
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
def receiver_guide() -> str:
|
|
@@ -37,20 +46,12 @@ def _front(text: str) -> dict[str, str]:
|
|
|
37
46
|
return found
|
|
38
47
|
|
|
39
48
|
|
|
40
|
-
def render_package(repo: Path, origin: str) -> str:
|
|
41
|
-
"""Compile, then render
|
|
49
|
+
def render_package(repo: Path, origin: str, today: date | None = None) -> str:
|
|
50
|
+
"""Compile, then render the same origin projection startup prints, plus the ledger."""
|
|
42
51
|
repo = Path(repo)
|
|
43
52
|
origin = safe_origin(origin)
|
|
44
|
-
compile_repo(repo)
|
|
45
|
-
|
|
46
|
-
card_path = origin_markdown(repo / "environments", origin)
|
|
47
|
-
card = card_path.read_text(encoding="utf-8") if card_path.is_file() else "(无)\n"
|
|
48
|
-
role = _front(card).get("role", "")
|
|
49
|
-
filename = scenario_filename(role) if role else None
|
|
50
|
-
scenario_path = repo / "views" / filename if filename else None
|
|
51
|
-
if scenario_path is not None and scenario_path.resolve().parent != (repo / "views").resolve():
|
|
52
|
-
scenario_path = None
|
|
53
|
-
scenario = scenario_path.read_text(encoding="utf-8") if scenario_path and scenario_path.is_file() else "(无)\n"
|
|
53
|
+
compile_repo(repo, today=today)
|
|
54
|
+
body = projection_body(repo, origin, today=today).rstrip("\n")
|
|
54
55
|
outbox_path = origin_markdown(repo / "outbox", origin)
|
|
55
56
|
ops_text = outbox_path.read_text(encoding="utf-8") if outbox_path.is_file() else ""
|
|
56
57
|
op_blocks = [op.raw.rstrip("\n") for op in parse_outbox(ops_text, origin)]
|
|
@@ -58,7 +59,7 @@ def render_package(repo: Path, origin: str) -> str:
|
|
|
58
59
|
version = compiler_version()
|
|
59
60
|
lines = [
|
|
60
61
|
"---",
|
|
61
|
-
'handoff_version: "
|
|
62
|
+
f'handoff_version: "{PACKAGE_VERSION}"',
|
|
62
63
|
"language: zh-CN",
|
|
63
64
|
"scope: private",
|
|
64
65
|
f"origin_id: {origin}",
|
|
@@ -69,15 +70,7 @@ def render_package(repo: Path, origin: str) -> str:
|
|
|
69
70
|
"",
|
|
70
71
|
"# 交接包",
|
|
71
72
|
"",
|
|
72
|
-
|
|
73
|
-
"",
|
|
74
|
-
shared.rstrip("\n"),
|
|
75
|
-
"",
|
|
76
|
-
scenario.rstrip("\n"),
|
|
77
|
-
"",
|
|
78
|
-
"## 环境卡片",
|
|
79
|
-
"",
|
|
80
|
-
card.rstrip("\n"),
|
|
73
|
+
body,
|
|
81
74
|
"",
|
|
82
75
|
"## 10. 操作附录(已反映在上文视图中)",
|
|
83
76
|
"",
|
|
@@ -92,39 +85,16 @@ def render_package(repo: Path, origin: str) -> str:
|
|
|
92
85
|
return "\n".join(lines)
|
|
93
86
|
|
|
94
87
|
|
|
95
|
-
def
|
|
96
|
-
""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
claim_id = claim.get("id")
|
|
102
|
-
line = f"- {text}" + (f" {{id:{claim_id}}}" if claim_id else "")
|
|
103
|
-
grouped.setdefault(section, []).append(line)
|
|
104
|
-
version = compiler_version()
|
|
105
|
-
lines = [
|
|
106
|
-
"---",
|
|
107
|
-
'handoff_version: "1.1"',
|
|
108
|
-
"language: zh-CN",
|
|
109
|
-
"scope: private",
|
|
110
|
-
f"mode: {mode}",
|
|
111
|
-
f'generator: "topmind-handoff {version}"',
|
|
112
|
-
"---",
|
|
113
|
-
"",
|
|
114
|
-
receiver_guide(),
|
|
115
|
-
"",
|
|
116
|
-
"# 交接包",
|
|
117
|
-
"",
|
|
118
|
-
]
|
|
119
|
-
for section, items in grouped.items():
|
|
120
|
-
lines.append(f"## {section}")
|
|
121
|
-
lines.extend(items)
|
|
122
|
-
lines.append("")
|
|
123
|
-
return "\n".join(lines)
|
|
88
|
+
def _ops_fence(package: str) -> re.Match[str] | None:
|
|
89
|
+
return re.search(r"```ops\n(.*?)```", package, re.S)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _has_ops_appendix(package: str) -> bool:
|
|
93
|
+
return _ops_fence(package) is not None
|
|
124
94
|
|
|
125
95
|
|
|
126
96
|
def _appendix_ops(package: str, origin: str):
|
|
127
|
-
match =
|
|
97
|
+
match = _ops_fence(package)
|
|
128
98
|
if not match:
|
|
129
99
|
return []
|
|
130
100
|
return parse_outbox(match.group(1), origin)
|
|
@@ -149,10 +119,11 @@ def replay_package(repo: Path, package: str, *, confirmed: bool = False) -> list
|
|
|
149
119
|
repo = Path(repo)
|
|
150
120
|
front = _front(package)
|
|
151
121
|
version = front.get("handoff_version", "")
|
|
152
|
-
if version
|
|
153
|
-
raise ValueError(f"
|
|
122
|
+
if version != PACKAGE_VERSION:
|
|
123
|
+
raise ValueError(f"不支持的交接包版本 {version or '(空)'}")
|
|
124
|
+
if not _has_ops_appendix(package):
|
|
125
|
+
raise ValueError("交接包没有操作附录")
|
|
154
126
|
origin = safe_origin(front.get("origin_id") or "unknown")
|
|
155
|
-
dest = origin_markdown(repo / "outbox", origin)
|
|
156
127
|
incoming = _appendix_ops(package, origin)
|
|
157
128
|
known = _known_ids(repo)
|
|
158
129
|
added: list[str] = []
|
|
@@ -165,11 +136,7 @@ def replay_package(repo: Path, package: str, *, confirmed: bool = False) -> list
|
|
|
165
136
|
blocks.append(op.raw.rstrip("\n"))
|
|
166
137
|
if not blocks or not confirmed:
|
|
167
138
|
return added
|
|
168
|
-
|
|
169
|
-
existing = dest.read_text(encoding="utf-8") if dest.is_file() else f"# outbox {origin}\n"
|
|
170
|
-
if not existing.endswith("\n"):
|
|
171
|
-
existing += "\n"
|
|
172
|
-
dest.write_text(existing + "\n".join(blocks) + "\n", encoding="utf-8")
|
|
139
|
+
append_outbox(repo, origin, origin, blocks)
|
|
173
140
|
compile_repo(repo)
|
|
174
141
|
return added
|
|
175
142
|
|
|
@@ -183,17 +150,23 @@ class LegacyPlan:
|
|
|
183
150
|
unkeyed: list[str] = field(default_factory=list)
|
|
184
151
|
suggested_deletions: list[str] = field(default_factory=list)
|
|
185
152
|
omitted_local_ids: list[str] = field(default_factory=list)
|
|
186
|
-
mode: str = "migration"
|
|
187
153
|
|
|
188
154
|
|
|
189
155
|
_SECTION_6 = re.compile(r"^##\s*6(?:\.|\s|$)")
|
|
190
156
|
|
|
191
157
|
|
|
158
|
+
def _skip_heading(stripped: str) -> bool:
|
|
159
|
+
"""Boundaries are behavior, not facts. The rendered slice heading is ## 边界."""
|
|
160
|
+
if _SECTION_6.match(stripped):
|
|
161
|
+
return True
|
|
162
|
+
return stripped.lstrip("#").strip() == "边界"
|
|
163
|
+
|
|
164
|
+
|
|
192
165
|
def _claim_items(package: str) -> list[tuple[str | None, str]]:
|
|
193
|
-
"""List items outside
|
|
166
|
+
"""List items outside boundary sections and outside fenced blocks.
|
|
194
167
|
|
|
195
|
-
Section 6
|
|
196
|
-
operations appendix, which is a ledger and not a new claim.
|
|
168
|
+
Section 6 and the rendered ## 边界 slice are behavior, not memory.
|
|
169
|
+
Fenced blocks hold the operations appendix, which is a ledger and not a new claim.
|
|
197
170
|
"""
|
|
198
171
|
items: list[tuple[str | None, str]] = []
|
|
199
172
|
skip = False
|
|
@@ -206,11 +179,11 @@ def _claim_items(package: str) -> list[tuple[str | None, str]]:
|
|
|
206
179
|
if fenced:
|
|
207
180
|
continue
|
|
208
181
|
if stripped.startswith("##"):
|
|
209
|
-
skip =
|
|
182
|
+
skip = _skip_heading(stripped)
|
|
210
183
|
continue
|
|
211
184
|
if skip or not stripped.startswith("- "):
|
|
212
185
|
continue
|
|
213
|
-
body = stripped[2:].strip()
|
|
186
|
+
body = _without_review_mark(stripped[2:].strip())
|
|
214
187
|
match = re.search(r"\{id:([^}]+)\}\s*$", body)
|
|
215
188
|
claim_id = match.group(1) if match else None
|
|
216
189
|
if match:
|
|
@@ -219,6 +192,13 @@ def _claim_items(package: str) -> list[tuple[str | None, str]]:
|
|
|
219
192
|
return items
|
|
220
193
|
|
|
221
194
|
|
|
195
|
+
def _without_review_mark(body: str) -> str:
|
|
196
|
+
"""A due line ends with the review label after `{id:…}`. The label is not claim text."""
|
|
197
|
+
if body.endswith(REVIEW_MARK):
|
|
198
|
+
return body[: -len(REVIEW_MARK)].rstrip()
|
|
199
|
+
return body
|
|
200
|
+
|
|
201
|
+
|
|
222
202
|
def _anchor_of(text: str) -> tuple[str, str] | None:
|
|
223
203
|
"""An anchor line starts with the field name. A later mention is not an anchor."""
|
|
224
204
|
stripped = text.strip()
|
|
@@ -236,15 +216,17 @@ def _is_tombstone(text: str) -> bool:
|
|
|
236
216
|
|
|
237
217
|
|
|
238
218
|
def legacy_import_plan(package: str, local: dict) -> LegacyPlan:
|
|
239
|
-
"""
|
|
219
|
+
"""Diff view lines into host memory. Existing text and anchors are not replaced."""
|
|
240
220
|
front = _front(package)
|
|
241
|
-
version = front.get("handoff_version", "
|
|
242
|
-
if
|
|
243
|
-
|
|
244
|
-
|
|
221
|
+
version = front.get("handoff_version", "")
|
|
222
|
+
if package.startswith("---"):
|
|
223
|
+
if version != PACKAGE_VERSION:
|
|
224
|
+
raise ValueError(f"不支持的交接包版本 {version or '(空)'}")
|
|
225
|
+
if not _has_ops_appendix(package):
|
|
226
|
+
raise ValueError("交接包没有操作附录")
|
|
245
227
|
local_anchors = dict(local.get("anchors") or {})
|
|
246
228
|
local_claims = dict(local.get("claims") or {})
|
|
247
|
-
plan = LegacyPlan(
|
|
229
|
+
plan = LegacyPlan()
|
|
248
230
|
seen: set[str] = set()
|
|
249
231
|
local_texts = {value for value in local_claims.values() if value}
|
|
250
232
|
for claim_id, text in _claim_items(package):
|