@sakanano-moe/agenthelm 6.7.0 → 6.11.0-rc.1

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 (86) hide show
  1. package/package.json +1 -1
  2. package/runtime/index.json +6 -6
  3. package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +1 -1
  4. package/runtime/plugins/agent-pack/hooks/hooks.json +32 -0
  5. package/runtime/plugins/agent-pack/plugin.json +1 -1
  6. package/runtime/plugins/agent-pack/references/local-only.md +126 -0
  7. package/runtime/plugins/agent-pack/references/nudge.en.md +58 -0
  8. package/runtime/plugins/agent-pack/references/nudge.md +58 -0
  9. package/runtime/plugins/agent-pack/runtime/agent_pack_nudge.py +573 -0
  10. package/runtime/plugins/agent-pack/runtime/agenthelm.py +419 -107
  11. package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +77 -53
  12. package/runtime/plugins/agent-pack/runtime/i18n_agent_pack.py +78 -0
  13. package/runtime/plugins/agent-pack/runtime/local_only.py +1130 -0
  14. package/runtime/plugins/agent-pack/runtime/locale/en.json +385 -0
  15. package/runtime/plugins/agent-pack/runtime/mcp_server.py +25 -0
  16. package/runtime/plugins/agent-pack/runtime/nudge_text.py +107 -0
  17. package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +9 -1
  18. package/runtime/plugins/agents-init/.codex-plugin/plugin.json +1 -1
  19. package/runtime/plugins/agents-init/content.json +17 -2
  20. package/runtime/plugins/agents-init/plugin.json +1 -1
  21. package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +47 -0
  22. package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +39 -1
  23. package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +404 -70
  24. package/runtime/plugins/agents-init/skills/agents-init/scripts/adopt.py +596 -0
  25. package/runtime/plugins/agents-init/skills/agents-init/scripts/i18n_init.py +78 -0
  26. package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +245 -72
  27. package/runtime/plugins/agents-init/skills/agents-init/scripts/locale/en.json +210 -0
  28. package/runtime/plugins/agents-init/skills/agents-init/scripts/regions.py +99 -0
  29. package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +1081 -0
  30. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_adopt.py +551 -0
  31. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +46 -2
  32. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +555 -0
  33. package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +66 -0
  34. package/runtime/plugins/agents-init/skills/agents-init/skill.json +1 -1
  35. package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +1 -1
  36. package/runtime/plugins/opinion-manager/plugin.json +1 -1
  37. package/runtime/plugins/opinion-manager/references/inbox.en.md +48 -0
  38. package/runtime/plugins/opinion-manager/references/inbox.md +48 -0
  39. package/runtime/plugins/opinion-manager/runtime/context.py +19 -18
  40. package/runtime/plugins/opinion-manager/runtime/i18n_opinion.py +78 -0
  41. package/runtime/plugins/opinion-manager/runtime/inbox.py +397 -0
  42. package/runtime/plugins/opinion-manager/runtime/inbox_text.py +101 -0
  43. package/runtime/plugins/opinion-manager/runtime/lifecycle.py +45 -44
  44. package/runtime/plugins/opinion-manager/runtime/locale/en.json +184 -0
  45. package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +55 -50
  46. package/runtime/plugins/opinion-manager/runtime/versioned.py +84 -83
  47. package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +15 -2
  48. package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +1 -1
  49. package/runtime/plugins/project-orchestrator/content.json +30 -3
  50. package/runtime/plugins/project-orchestrator/plugin.json +1 -1
  51. package/runtime/plugins/project-orchestrator/references/case-workspaces.md +9 -1
  52. package/runtime/plugins/project-orchestrator/references/case.schema.json +1 -0
  53. package/runtime/plugins/project-orchestrator/references/sop-interview.md +83 -0
  54. package/runtime/plugins/project-orchestrator/references/sop.md +156 -0
  55. package/runtime/plugins/project-orchestrator/runtime/assets.py +52 -57
  56. package/runtime/plugins/project-orchestrator/runtime/case.py +139 -80
  57. package/runtime/plugins/project-orchestrator/runtime/case_structure.py +71 -0
  58. package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +47 -46
  59. package/runtime/plugins/project-orchestrator/runtime/i18n_orchestrator.py +78 -0
  60. package/runtime/plugins/project-orchestrator/runtime/locale/en.json +1409 -0
  61. package/runtime/plugins/project-orchestrator/runtime/memory.py +44 -43
  62. package/runtime/plugins/project-orchestrator/runtime/private_paths.py +119 -0
  63. package/runtime/plugins/project-orchestrator/runtime/project_base.py +37 -19
  64. package/runtime/plugins/project-orchestrator/runtime/project_cli.py +267 -174
  65. package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
  66. package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +524 -134
  67. package/runtime/plugins/project-orchestrator/runtime/project_menu.py +159 -85
  68. package/runtime/plugins/project-orchestrator/runtime/project_real.py +191 -106
  69. package/runtime/plugins/project-orchestrator/runtime/project_write.py +58 -55
  70. package/runtime/plugins/project-orchestrator/runtime/sop.py +1670 -0
  71. package/runtime/plugins/project-orchestrator/runtime/sop_structure.py +226 -0
  72. package/runtime/plugins/project-orchestrator/runtime/wf.py +74 -70
  73. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +22 -0
  74. package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.en.md +57 -0
  75. package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.md +57 -0
  76. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.en.md +77 -0
  77. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.md +77 -0
  78. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.en.md +11 -0
  79. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.md +11 -0
  80. package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.en.md +87 -0
  81. package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.md +87 -0
  82. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.en.md +75 -0
  83. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.md +75 -0
  84. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.en.md +11 -0
  85. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.md +12 -0
  86. package/runtime/plugins/project-orchestrator/templates/CASE.en.md +39 -0
@@ -8,7 +8,8 @@
8
8
  扫描范围来自 Project Profile:Case 根取 ``work.case.root``(缺省 ``cases``),额外扫描根取
9
9
  ``work.assets.roots``(缺省无)。同义词来自项目可选文件
10
10
  ``.agents/moe.sakanano.agent-pack/asset-synonyms.json``(缺省为空)。凭据、密钥与证书
11
- 无论是否入库都不登记;嵌套的 SVN/Git 外部检出、纯缓存目录与非 shared 的 Case 不展开。
11
+ 无论是否入库都不登记;嵌套的 SVN/Git 外部检出与纯缓存目录不展开。Case 的 sensitivity 不影响登记:台账只是本机缓存里的索引,
12
+ private 的 Case 也能被找到(读取其内容前是否需要确认,由私有目录的约定管,不在这里过滤)。
12
13
  """
13
14
  from __future__ import annotations
14
15
 
@@ -26,6 +27,7 @@ import time
26
27
  from pathlib import Path
27
28
  from typing import Any, Iterator
28
29
  from urllib.parse import unquote
30
+ from i18n_orchestrator import _ # noqa: E402
29
31
 
30
32
  try:
31
33
  import fcntl
@@ -34,7 +36,7 @@ except ImportError: # Windows
34
36
 
35
37
  PROFILE = Path(".agents/moe.sakanano.agent-pack/project.json")
36
38
  SYNONYMS = Path(".agents/moe.sakanano.agent-pack/asset-synonyms.json")
37
- ASSET_SCHEMA_VERSION = 1
39
+ ASSET_SCHEMA_VERSION = 2 # 2: 非 shared 的 Case 也登记;旧台账自动重建
38
40
  DB_NAME = "assets.sqlite3"
39
41
  LOCK_NAME = "assets.lock"
40
42
  LAST_NAME = "wf-last.json"
@@ -114,7 +116,7 @@ class Project:
114
116
  def __init__(self, root: Path) -> None:
115
117
  self.root = root.expanduser().resolve()
116
118
  if not self.root.is_dir():
117
- raise AssetError(f"项目目录不存在:{root}", "project-missing")
119
+ raise AssetError(_("项目目录不存在:{0}").format(root), "project-missing")
118
120
  profile_path = self.root / PROFILE
119
121
  self.profile_found = profile_path.is_file()
120
122
  profile: dict = {}
@@ -122,7 +124,7 @@ class Project:
122
124
  try:
123
125
  profile = json.loads(profile_path.read_text(encoding="utf-8"))
124
126
  except (OSError, ValueError) as exc:
125
- raise AssetError(f"Project Profile 无法解析:{exc}", "profile-invalid") from exc
127
+ raise AssetError(_("Project Profile 无法解析:{0}").format(exc), "profile-invalid") from exc
126
128
  work = profile.get("work") if isinstance(profile, dict) else None
127
129
  work = work if isinstance(work, dict) else {}
128
130
  case = work.get("case") if isinstance(work.get("case"), dict) else {}
@@ -130,7 +132,7 @@ class Project:
130
132
  self.case_root = self._relative(case.get("root") or "cases", "work.case.root")
131
133
  extra = assets.get("roots") or []
132
134
  if not isinstance(extra, list) or not all(isinstance(item, str) for item in extra):
133
- raise AssetError("work.assets.roots 必须是字符串列表", "profile-invalid")
135
+ raise AssetError(_("work.assets.roots 必须是字符串列表"), "profile-invalid")
134
136
  roots = [self.case_root]
135
137
  for value in extra:
136
138
  relative = self._relative(value, "work.assets.roots")
@@ -139,7 +141,7 @@ class Project:
139
141
  for first in roots:
140
142
  for second in roots:
141
143
  if first != second and second.startswith(first + "/"):
142
- raise AssetError(f"扫描根互相嵌套:{first} 与 {second}", "profile-invalid")
144
+ raise AssetError(_("扫描根互相嵌套:{0} 与 {1}").format(first, second), "profile-invalid")
143
145
  self.roots = roots
144
146
 
145
147
  @classmethod
@@ -154,15 +156,15 @@ class Project:
154
156
 
155
157
  def _relative(self, value: Any, label: str) -> str:
156
158
  if not isinstance(value, str):
157
- raise AssetError(f"{label} 必须是项目内相对路径", "profile-invalid")
159
+ raise AssetError(_("{0} 必须是项目内相对路径").format(label), "profile-invalid")
158
160
  relative = Path(value)
159
161
  if relative.is_absolute() or ".." in relative.parts or not relative.parts:
160
- raise AssetError(f"{label} 必须是项目内相对路径", "profile-invalid")
162
+ raise AssetError(_("{0} 必须是项目内相对路径").format(label), "profile-invalid")
161
163
  current = self.root
162
164
  for part in relative.parts:
163
165
  current = current / part
164
166
  if current.is_symlink():
165
- raise AssetError(f"{label} 不能经过符号链接", "symlink")
167
+ raise AssetError(_("{0} 不能经过符号链接").format(label), "symlink")
166
168
  return relative.as_posix()
167
169
 
168
170
  def state_dir(self, create: bool = False) -> Path:
@@ -172,7 +174,7 @@ class Project:
172
174
  directory = cache / key
173
175
  resolved = directory.resolve()
174
176
  if resolved == self.root or self.root in resolved.parents:
175
- raise AssetError("缓存目录不能位于项目内(检查 XDG_CACHE_HOME)", "state-inside-project")
177
+ raise AssetError(_("缓存目录不能位于项目内(检查 XDG_CACHE_HOME)"), "state-inside-project")
176
178
  if create:
177
179
  directory.mkdir(parents=True, exist_ok=True, mode=0o700)
178
180
  try:
@@ -181,7 +183,7 @@ class Project:
181
183
  pass
182
184
  for path in (directory, directory / DB_NAME, directory / LOCK_NAME, directory / LAST_NAME):
183
185
  if path.is_symlink():
184
- raise AssetError(f"缓存路径不能是符号链接:{path.name}", "symlink")
186
+ raise AssetError(_("缓存路径不能是符号链接:{0}").format(path.name), "symlink")
185
187
  return directory
186
188
 
187
189
  def synonyms(self) -> list[list[str]]:
@@ -191,10 +193,10 @@ class Project:
191
193
  try:
192
194
  data = json.loads(path.read_text(encoding="utf-8"))
193
195
  except (OSError, ValueError) as exc:
194
- raise AssetError(f"同义词文件无法解析:{exc}", "synonyms-invalid") from exc
196
+ raise AssetError(_("同义词文件无法解析:{0}").format(exc), "synonyms-invalid") from exc
195
197
  groups = data.get("groups") if isinstance(data, dict) else None
196
198
  if not isinstance(groups, list):
197
- raise AssetError("同义词文件需要 groups 列表", "synonyms-invalid")
199
+ raise AssetError(_("同义词文件需要 groups 列表"), "synonyms-invalid")
198
200
  return [[str(word).lower() for word in group if str(word).strip()]
199
201
  for group in groups if isinstance(group, list) and len(group) > 1]
200
202
 
@@ -535,21 +537,17 @@ def scan(project: Project, previous: dict | None = None) -> dict:
535
537
  previous = previous or {}
536
538
  root = project.root
537
539
  containers = _load_containers(project)
538
- non_shared = {cid for cid, item in containers.items() if item["sensitivity"] != "shared"}
539
540
  skipped: list[dict] = []
540
541
  checkouts: list[str] = []
541
542
  raw = []
542
543
  for rel, stat in _walk(project, skipped, checkouts):
543
- container, kind, _ = project.container_of(rel)
544
- if kind == "case" and container in non_shared:
545
- continue
546
544
  raw.append((rel, stat))
547
545
  external = _checkout_roots(project, checkouts)
548
546
  for directory in sorted(external):
549
547
  skipped.append({"reason": "external-checkout", "path": directory})
550
548
  if external:
551
549
  raw = [(rel, stat) for rel, stat in raw if not any(rel.startswith(item + "/") for item in external)]
552
- has_git, tracked, last_commit, dirty, ignored = _git_state(project, [rel for rel, _ in raw])
550
+ has_git, tracked, last_commit, dirty, ignored = _git_state(project, [rel for rel, _stat in raw])
553
551
 
554
552
  files = {}
555
553
  denied = 0
@@ -598,7 +596,7 @@ def scan(project: Project, previous: dict | None = None) -> dict:
598
596
  }
599
597
  edges, broken = _extract_edges(project, files, containers)
600
598
  return {"files": files, "containers": containers, "edges": edges, "broken": broken,
601
- "non_shared": sorted(non_shared), "skipped": skipped, "denied": denied, "git": has_git}
599
+ "skipped": skipped, "denied": denied, "git": has_git}
602
600
 
603
601
 
604
602
  def _prefixes(project: Project) -> tuple[str, ...]:
@@ -721,7 +719,7 @@ def _extract_edges(project: Project, files: dict, containers: dict) -> tuple[dic
721
719
  elif looks and match.group(1) not in seen:
722
720
  broken.setdefault((src, match.group(1)), "link")
723
721
  for match in WIKI.finditer(text):
724
- dst, _ = resolve(match.group(1), src, "wiki")
722
+ dst, _kind = resolve(match.group(1), src, "wiki")
725
723
  if dst:
726
724
  add_edge(src, dst, "wikilink", match.group(1))
727
725
  else:
@@ -784,7 +782,7 @@ class _Lock:
784
782
  try:
785
783
  self.fd = os.open(self.path, flags, 0o600)
786
784
  except OSError as exc:
787
- raise AssetError(f"无法打开台账锁:{exc}", "state-lock") from exc
785
+ raise AssetError(_("无法打开台账锁:{0}").format(exc), "state-lock") from exc
788
786
  try:
789
787
  if fcntl is not None:
790
788
  fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
@@ -794,7 +792,7 @@ class _Lock:
794
792
  except OSError as exc:
795
793
  os.close(self.fd)
796
794
  self.fd = None
797
- raise AssetError("台账正在被另一个进程重建,请稍后重试", "index-locked") from exc
795
+ raise AssetError(_("台账正在被另一个进程重建,请稍后重试"), "index-locked") from exc
798
796
  return self
799
797
 
800
798
  def __exit__(self, *_: Any) -> None:
@@ -828,7 +826,6 @@ def _write_database(path: Path, registry: dict, fingerprint: str, project: Proje
828
826
  "project": str(project.root), "roots": json.dumps(project.roots, ensure_ascii=False),
829
827
  "file_count": str(len(registry["files"])), "edge_count": str(len(registry["edges"])),
830
828
  "denied": str(registry["denied"]), "git": "1" if registry["git"] else "0",
831
- "non_shared": json.dumps(registry["non_shared"]),
832
829
  "skipped": json.dumps(registry["skipped"], ensure_ascii=False)}
833
830
  connection.executemany("INSERT INTO meta VALUES (?, ?)", meta.items())
834
831
  connection.executemany(
@@ -860,16 +857,16 @@ def _write_database(path: Path, registry: dict, fingerprint: str, project: Proje
860
857
  def _open(project: Project) -> sqlite3.Connection:
861
858
  path = project.state_dir() / DB_NAME
862
859
  if not path.is_file():
863
- raise AssetError("台账尚未建立,先运行 rebuild(或去掉 --no-refresh)", "index-missing")
860
+ raise AssetError(_("台账尚未建立,先运行 rebuild(或去掉 --no-refresh)"), "index-missing")
864
861
  try:
865
862
  connection = sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)
866
863
  connection.row_factory = sqlite3.Row
867
864
  version = connection.execute("PRAGMA user_version").fetchone()[0]
868
865
  except sqlite3.Error as exc:
869
- raise AssetError("台账无法读取,请运行 rebuild --force", "index-corrupt") from exc
866
+ raise AssetError(_("台账无法读取,请运行 rebuild --force"), "index-corrupt") from exc
870
867
  if version != ASSET_SCHEMA_VERSION:
871
868
  connection.close()
872
- raise AssetError("台账格式与当前工具不一致,请运行 rebuild --force", "index-incompatible")
869
+ raise AssetError(_("台账格式与当前工具不一致,请运行 rebuild --force"), "index-incompatible")
873
870
  return connection
874
871
 
875
872
 
@@ -935,6 +932,7 @@ def _row_info(row: sqlite3.Row, info: dict, inbound: collections.Counter) -> dic
935
932
  return {"path": row["path"], "name": row["name"], "title": row["title"], "role": row["role"],
936
933
  "container": row["container"], "container_kind": row["container_kind"],
937
934
  "case_title": container.get("title", ""), "status": container.get("status", ""),
935
+ "sensitivity": container.get("sensitivity", "shared"),
938
936
  "modified": _iso(row["mtime_ns"] / 1e9), "size": row["size"],
939
937
  "git": row["git_state"] + ("+dirty" if row["git_dirty"] else ""),
940
938
  "dataless": bool(row["dataless"]), "referenced_by": inbound.get(row["path"], 0)}
@@ -1025,7 +1023,7 @@ def recent(project: Project, *, limit: int = 15, refresh: bool = True) -> dict:
1025
1023
  info, inbound = _containers(connection), _inbound_counts(connection)
1026
1024
  rows = connection.execute("SELECT * FROM files WHERE role IN ('deliverable','draft','project-file') "
1027
1025
  "ORDER BY mtime_ns DESC LIMIT 400").fetchall()
1028
- groups, _ = _group_families([(0, row["mtime_ns"], row) for row in rows], info, inbound, limit)
1026
+ groups, _total = _group_families([(0, row["mtime_ns"], row) for row in rows], info, inbound, limit)
1029
1027
  return {"ok": True, "results": groups, "returned": len(groups)}
1030
1028
  finally:
1031
1029
  connection.close()
@@ -1040,7 +1038,7 @@ def cases(project: Project, term: str | None = None, *, limit: int = 30, refresh
1040
1038
  for row in connection.execute("SELECT * FROM containers WHERE kind = 'case'"):
1041
1039
  haystack = " ".join([row["id"], row["title"], row["goal"], row["tags"], row["aliases"]]).lower()
1042
1040
  if term:
1043
- words, _ = parse_query([term])
1041
+ words, _dates = parse_query([term])
1044
1042
  if not all(any(word in haystack for word in alternatives)
1045
1043
  for alternatives in expand_terms(words, groups)):
1046
1044
  continue
@@ -1073,7 +1071,7 @@ def refs(project: Project, path: str, *, depth: int = 1, refresh: bool = True) -
1073
1071
  if not connection.execute("SELECT 1 FROM files WHERE path = ?", (path,)).fetchone() and \
1074
1072
  not connection.execute("SELECT 1 FROM edges WHERE dst = ? OR dst = ?",
1075
1073
  (path, path.rstrip("/") + "/")).fetchone():
1076
- raise AssetError(f"台账里没有这个路径:{path}", "unknown-path")
1074
+ raise AssetError(_("台账里没有这个路径:{0}").format(path), "unknown-path")
1077
1075
  outbound = [{"dst": row["dst"], "type": row["type"]}
1078
1076
  for row in connection.execute("SELECT dst, type FROM edges WHERE src = ? ORDER BY dst", (path,))]
1079
1077
  direct, seen_src = [], set()
@@ -1083,7 +1081,7 @@ def refs(project: Project, path: str, *, depth: int = 1, refresh: bool = True) -
1083
1081
  direct.append(row)
1084
1082
  layers, seen = [direct], {path} | seen_src
1085
1083
  frontier = [row["src"] for row in direct]
1086
- for _ in range(max(depth, 1) - 1):
1084
+ for _depth in range(max(depth, 1) - 1):
1087
1085
  following = []
1088
1086
  for node in frontier:
1089
1087
  for row in _inbound_rows(connection, node):
@@ -1175,7 +1173,7 @@ def check(project: Project, *, limit: int = 50, refresh: bool = True) -> dict:
1175
1173
  # ---------------------------------------------------------------- cli
1176
1174
 
1177
1175
  def parser() -> argparse.ArgumentParser:
1178
- result = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
1176
+ result = argparse.ArgumentParser(description=_("交付物台账:登记 Case 与额外扫描根下的文件,按标题、日期、同义词查找,追踪版本与引用。"))
1179
1177
  result.add_argument("--project", type=Path, default=None)
1180
1178
  result.add_argument("--output", choices=("text", "json"), default="text")
1181
1179
  # The same options are accepted after the subcommand, where people tend to type them.
@@ -1187,28 +1185,28 @@ def parser() -> argparse.ArgumentParser:
1187
1185
  def command(name: str, help_text: str, refresh: bool = True) -> argparse.ArgumentParser:
1188
1186
  item = sub.add_parser(name, parents=[shared], help=help_text)
1189
1187
  if refresh:
1190
- item.add_argument("--no-refresh", action="store_true", help="不先增量重建,直接读现有台账")
1188
+ item.add_argument("--no-refresh", action="store_true", help=_("不先增量重建,直接读现有台账"))
1191
1189
  return item
1192
1190
 
1193
- build = command("rebuild", "重建台账(文件清单未变时为 no-op)", refresh=False)
1191
+ build = command("rebuild", _("重建台账(文件清单未变时为 no-op)"), refresh=False)
1194
1192
  build.add_argument("--force", action="store_true")
1195
- command("status", "台账是否最新、位置与扫描根", refresh=False)
1196
- search = command("find", "按标题、文件名、日期与同义词查找,同一系列的版本归为一组")
1193
+ command("status", _("台账是否最新、位置与扫描根"), refresh=False)
1194
+ search = command("find", _("按标题、文件名、日期与同义词查找,同一系列的版本归为一组"))
1197
1195
  search.add_argument("terms", nargs="*")
1198
- search.add_argument("--kind", help="角色:deliverable / draft / source / data / image / script / process / project-file")
1196
+ search.add_argument("--kind", help=_("角色:deliverable / draft / source / data / image / script / process / project-file"))
1199
1197
  search.add_argument("--ext")
1200
- search.add_argument("--in", dest="container", help="限定 Case 或目录(片段匹配)")
1201
- search.add_argument("--content", action="store_true", help="名称不命中时再查文本正文(≤1 MiB)")
1198
+ search.add_argument("--in", dest="container", help=_("限定 Case 或目录(片段匹配)"))
1199
+ search.add_argument("--content", action="store_true", help=_("名称不命中时再查文本正文(≤1 MiB)"))
1202
1200
  search.add_argument("--limit", type=int, default=10)
1203
- latest = command("recent", "最近改动的交付物")
1201
+ latest = command("recent", _("最近改动的交付物"))
1204
1202
  latest.add_argument("--limit", type=int, default=15)
1205
- listing = command("cases", "Case 列表(进行中在前)与各自登记的文件数")
1203
+ listing = command("cases", _("Case 列表(进行中在前)与各自登记的文件数"))
1206
1204
  listing.add_argument("term", nargs="?")
1207
1205
  listing.add_argument("--limit", type=int, default=30)
1208
- reference = command("refs", "谁引用它、它引用谁、改动会波及哪些 Case")
1206
+ reference = command("refs", _("谁引用它、它引用谁、改动会波及哪些 Case"))
1209
1207
  reference.add_argument("path")
1210
1208
  reference.add_argument("--depth", type=int, default=1)
1211
- inspect = command("check", "巡检:无人认领的交付物、断链、跨容器重复、上游更新")
1209
+ inspect = command("check", _("巡检:无人认领的交付物、断链、跨容器重复、上游更新"))
1212
1210
  inspect.add_argument("--limit", type=int, default=50)
1213
1211
  return result
1214
1212
 
@@ -1219,32 +1217,29 @@ def render_text(command: str, result: dict) -> str:
1219
1217
  for index, item in enumerate(result["results"], 1):
1220
1218
  owner = item["case_title"] or item["container"]
1221
1219
  lines.append(f"{index:>2}. {item['title'] or item['name']} [{owner}] {item['git']} {item['path']}")
1222
- lines += [f" 版本 {version['name']} {version['path']}" for version in item["versions"]]
1223
- return "\n".join(lines) or "没有找到。"
1220
+ lines += [_(" 版本 {0} {1}").format(version['name'], version['path']) for version in item["versions"]]
1221
+ return "\n".join(lines) or _("没有找到。")
1224
1222
  if command == "cases":
1225
- return "\n".join(f"{item['status'] or '-':9} {item['id']} {item['title']} ({item['files']} 个文件)"
1226
- for item in result["results"]) or "没有 Case。"
1223
+ return "\n".join(_("{0:9} {1} {2} ({3} 个文件)").format(item['status'] or '-', item['id'], item['title'], item['files'])
1224
+ for item in result["results"]) or _("没有 Case。")
1227
1225
  if command == "refs":
1228
1226
  lines = [result["path"]]
1229
1227
  lines += [f" ← {item['src']}" + ("" if item["via"] == "file" else f" ({item['via']})")
1230
1228
  for item in result["inbound"]]
1231
1229
  lines += [f" → {item['dst']}" for item in result["outbound"]]
1232
1230
  if result["affected_containers"]:
1233
- lines.append(" 改动会波及:" + "、".join(result["affected_containers"]))
1231
+ lines.append(_(" 改动会波及:") + _("、").join(result["affected_containers"]))
1234
1232
  return "\n".join(lines)
1235
1233
  if command == "check":
1236
1234
  totals, tiers, counts = result["totals"], result["claim_tiers"], result["broken_counts"]
1237
1235
  git = totals["git"]
1238
- lines = [f"文件 {totals['files']} · 引用 {totals['edges']} · 拒绝登记的凭据类文件 {totals['denied']}",
1239
- f"Git:已入库 {git.get('tracked', 0)} / 被忽略 {git.get('ignored', 0)} / "
1240
- f"未提交 {git.get('untracked', 0)} / 非 Git {git.get('none', 0)}",
1241
- f"交付物:文件级被引用 {tiers.get('file', 0)} · 仅目录覆盖 {tiers.get('directory', 0)} · "
1242
- f"无人认领 {tiers.get('none', 0)}",
1243
- f"断链:{counts.get('real-broken', 0)} · 说明文字 {counts.get('prose-not-path', 0)} · "
1244
- f"旧 Task 路径 {counts.get('legacy-task-id', 0)} · 主题链接 {counts.get('wikilink-topic', 0)}"]
1245
- lines += [f" 断链 {item['src']} → {item['raw']}" for item in result["broken"].get("real-broken", [])]
1246
- lines += [f" 无人认领 {path}" for paths in result["unclaimed"].values() for path in paths]
1247
- lines += [" 重复 " + " ≡ ".join(group) for group in result["duplicates"]]
1236
+ lines = [_("文件 {0} · 引用 {1} · 拒绝登记的凭据类文件 {2}").format(totals['files'], totals['edges'], totals['denied']),
1237
+ _("Git:已入库 {0} / 被忽略 {1} / 未提交 {2} / 非 Git {3}").format(git.get('tracked', 0), git.get('ignored', 0), git.get('untracked', 0), git.get('none', 0)),
1238
+ _("交付物:文件级被引用 {0} · 仅目录覆盖 {1} · 无人认领 {2}").format(tiers.get('file', 0), tiers.get('directory', 0), tiers.get('none', 0)),
1239
+ _("断链:{0} · 说明文字 {1} · 旧 Task 路径 {2} · 主题链接 {3}").format(counts.get('real-broken', 0), counts.get('prose-not-path', 0), counts.get('legacy-task-id', 0), counts.get('wikilink-topic', 0))]
1240
+ lines += [_(" 断链 {0} → {1}").format(item['src'], item['raw']) for item in result["broken"].get("real-broken", [])]
1241
+ lines += [_(" 无人认领 {0}").format(path) for paths in result["unclaimed"].values() for path in paths]
1242
+ lines += [_(" 重复 ") + " ≡ ".join(group) for group in result["duplicates"]]
1248
1243
  return "\n".join(lines)
1249
1244
  return json.dumps(result, ensure_ascii=False, indent=2)
1250
1245