@sakanano-moe/agenthelm 6.8.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 +228 -147
  11. package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +64 -59
  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 +4 -4
  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 +26 -1
  23. package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +399 -80
  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 +210 -76
  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 +210 -62
  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 +4 -0
  33. package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +2 -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 +222 -189
  65. package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
  66. package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +241 -204
  67. package/runtime/plugins/project-orchestrator/runtime/project_menu.py +145 -89
  68. package/runtime/plugins/project-orchestrator/runtime/project_real.py +184 -105
  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
@@ -0,0 +1,226 @@
1
+ """SOP 文件的结构:小节名、字段名、前置条件的写法与写进 Case 的文字,按项目文件语言(Project Profile 的 ``language``)。
2
+
3
+ 和 Case 一样(见 case_structure.py),SOP 写进项目的文件用什么语言是项目的属性,随项目同步,不随命令行界面语言
4
+ (AGENTHELM_LANG)变化:``language`` 为 ``en`` 的项目新建的 SOP 用英文小节名与字段名,缺省或 ``zh`` 的项目保持中文。
5
+ 读取时两套词表都认,所以手写的、从别的项目拷来的、换过语言的 SOP 都能照常解析;追加内容时沿用文件自己的写法。
6
+
7
+ frontmatter 的键、枚举值(ask / stop / retry …)本来就是英文,与语言无关,不在这里。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import re
12
+
13
+ import case_structure
14
+
15
+ LANGUAGES = case_structure.LANGUAGES
16
+ language_of = case_structure.language_of
17
+
18
+ SECTIONS = {
19
+ "purpose": ("目的", "Purpose"),
20
+ "requires": ("前置条件", "Prerequisites"),
21
+ "steps": ("步骤", "Steps"),
22
+ "done": ("完成标准", "Completion criteria"),
23
+ "exceptions": ("例外与回退", "Exceptions and fallback"),
24
+ "revisions": ("修订记录", "Revision log"),
25
+ }
26
+ STRICT_SECTIONS = ("requires", "steps", "done")
27
+
28
+ STEP_KEYS = {
29
+ "tools": ("工具", "Tools"),
30
+ "inputs": ("输入", "Inputs"),
31
+ "intermediates": ("中间产物", "Intermediates"),
32
+ "finals": ("最终产物", "Final outputs"),
33
+ "checkpoint": ("检查点", "Checkpoint"),
34
+ "on_failure": ("失败时", "On failure"),
35
+ "confirm": ("需确认", "Needs confirmation"),
36
+ "note": ("说明", "Note"),
37
+ }
38
+ REQ_KINDS = {
39
+ "command": ("命令", "Command"),
40
+ "module": ("模块", "Module"),
41
+ "env": ("环境变量", "Environment variable"),
42
+ "path": ("路径", "Path"),
43
+ "skill": ("技能", "Skill"),
44
+ "mcp": ("MCP", "MCP"),
45
+ "manual": ("手动确认", "Manual check"),
46
+ }
47
+ REQ_KEYS = {
48
+ "purpose": ("用途", "Purpose"),
49
+ "install": ("安装", "Install"),
50
+ "alternative": ("替代", "Alternative"),
51
+ "if_missing": ("缺失时", "If missing"),
52
+ "version": ("版本", "Version"),
53
+ "when": ("仅当", "Only if"),
54
+ }
55
+ MISSING = {
56
+ "ask": ("询问用户", "Ask the user"),
57
+ "stop": ("停止", "Stop"),
58
+ "skip-step": ("跳过该步", "Skip the step"),
59
+ "alternative": ("使用替代", "Use the alternative"),
60
+ }
61
+ # 只在读取时认的别名(简写)。
62
+ KIND_ALIASES = {"手动": "manual"}
63
+ MISSING_ALIASES = {"询问": "ask", "跳过": "skip-step", "替代": "alternative", "ask": "ask", "skip": "skip-step"}
64
+
65
+ TRUE_WORDS = ("是", "y", "yes", "true", "需要", "required")
66
+ NONE_WORDS = ("无", "(无)", "(无)", "不需要", "none", "(none)", "n/a", "not needed")
67
+ LIST_NONE = ("无", "-", "—", "none")
68
+
69
+ PLACEHOLDERS = ("【待补充】", "[TO FILL IN]")
70
+ EMPTY_REVISIONS = ("暂无。", "None yet.")
71
+
72
+ # 执行 Case 里 SOP 自己加的小节(Case 的标准小节见 case_structure)。
73
+ CASE_SECTIONS = {
74
+ "checklist": ("SOP 执行清单", "SOP checklist"),
75
+ "intermediate": ("中间产物", "Intermediate artifacts"),
76
+ }
77
+
78
+ # 写进 Case 与 SOP 文件的文字。{…} 是 str.format 的字段。
79
+ TEXTS = {
80
+ "revision_line": ("{date} v{version}:{note}", "{date} v{version}: {note}"),
81
+ "note_activated": ("启用(内容与执行约定已与用户确认)", "Activated (content and run conventions confirmed with the user)"),
82
+ "note_retired": ("退役", "Retired"),
83
+ "note_restored": ("恢复", "Restored"),
84
+ "from_case_description": ("{p}(从 {case} 提炼)", "{p} (distilled from {case})"),
85
+ "from_case_step": ("{index}. {text}\n - 说明:{p}整理这一步(来自 {case} 的工作记录,需改写成可重复执行的动作)",
86
+ "{index}. {text}\n - Note: {p} tidy this step (from the work log of {case}; rewrite it as a repeatable action)"),
87
+ "case_created_marker": ("Case 已创建", "Case created"),
88
+ # 执行清单
89
+ "cl_source": ("来源:`{id}@{version}`({note})", "Source: `{id}@{version}` ({note})"),
90
+ "snapshot_note": ("快照 {path}", "Snapshot {path}"),
91
+ "cl_conventions": ("执行约定(开始时从 SOP 与项目习惯固化,执行中不随 SOP 改动):",
92
+ "Run conventions (frozen from the SOP and the project habits at the start; they do not follow later SOP edits):"),
93
+ "cl_on_error": ("- 出错时:", "- On error: "),
94
+ "cl_retries": ("(最多重试 {n} 次)", " (at most {n} retries)"),
95
+ "cl_on_missing": ("- 缺少工具时:", "- On a missing tool: "),
96
+ "cl_artifacts_label": ("- 中间产物:", "- Intermediate artifacts: "),
97
+ "cl_artifacts": ("放在 {place};完成后:{keep}", "kept in {place}; afterwards: {keep}"),
98
+ "cl_data_dir": ("本 Case 的 data/ 目录", "this Case's data/ directory"),
99
+ "cl_resume": ("- 失败后重新执行:", "- Resuming after a failure: "),
100
+ "cl_costly": ("- 花钱或耗时的步骤:", "- Costly or slow steps: "),
101
+ "cl_private": ("- 私有目录:本次会用到 {dirs};读取前须经用户确认(用 sop confirm-read 记录),内容默认不进入 git;本 Case 已标为 private",
102
+ "- Private directories: this run will use {dirs}; the user must confirm before each read (record it with sop confirm-read), the content stays out of git by default; this Case is marked private"),
103
+ "cl_params": ("参数:", "Parameters:"),
104
+ "cl_steps": ("步骤:", "Steps:"),
105
+ "cl_tools": ("(工具:{tools})", " (tools: {tools})"),
106
+ "cl_event_param": ("触发事件:{event}({id}:{title})", "Trigger event: {event} ({id}: {title})"),
107
+ "event_note": ("由 {event} 触发({id})", "Triggered by {event} ({id})"),
108
+ "source_event": (";{note}", "; {note}"),
109
+ "next_first": ("步骤 1:{title}", "Step 1: {title}"),
110
+ "progress0": ("SOP 进度 0/{total}", "SOP progress 0/{total}"),
111
+ "empty_intermediates": ("暂无。", "None yet."),
112
+ "log_started": ("{stamp} 按 {id}@{version} 开始执行{event};执行前检查:就绪 {ok}、缺失 {missing}、待核对 {unknown}",
113
+ "{stamp} Started running {id}@{version}{event}; preflight: {ok} ready, {missing} missing, {unknown} to verify"),
114
+ "log_started_event": (",{note}", ", {note}"),
115
+ # 逐步记录
116
+ "state_done": ("完成", "done"),
117
+ "state_skip": ("跳过", "skipped"),
118
+ "state_fail": ("失败", "failed"),
119
+ "state_pending": ("重置为待办", "reset to to-do"),
120
+ "log_step": ("{stamp} SOP 步骤 {n}「{title}」{label}{note}", "{stamp} SOP step {n} \"{title}\" {label}{note}"),
121
+ "note_join": (":{note}", ": {note}"),
122
+ "artifact_line": ("步骤 {n}:`{location}`", "Step {n}: `{location}`"),
123
+ "artifact_what": ("({what})", " ({what})"),
124
+ "next_failed": ("处理步骤 {n} 的失败(约定:{habit})", "Handle the failure of step {n} (convention: {habit})"),
125
+ "next_step": ("步骤 {n}:{title}", "Step {n}: {title}"),
126
+ "next_done": ("核对完成标准(sop.py status)后关闭 Case 或登记交付物",
127
+ "Check the completion criteria (sop.py status), then close the Case or register deliverables"),
128
+ "state_progress": ("SOP 进度 {done}/{total}", "SOP progress {done}/{total}"),
129
+ "state_failed_suffix": (",步骤 {n} 失败", ", step {n} failed"),
130
+ "log_confirm_read": ("{stamp} 用户确认读取私有路径:{paths}{note}", "{stamp} The user confirmed reading private paths: {paths}{note}"),
131
+ "list_join": ("、", ", "),
132
+ "confirm_note": (";{note}", "; {note}"),
133
+ }
134
+
135
+ _TOOLS_SPLIT = re.compile(r"(工具|\s\(tools:")
136
+
137
+
138
+ def text(key: str, language: str, /, **fields: object) -> str:
139
+ return TEXTS[key][LANGUAGES.index(language)].format(**fields)
140
+
141
+
142
+ def name(table: dict, key: str, language: str) -> str:
143
+ return table[key][LANGUAGES.index(language)]
144
+
145
+
146
+ def _lookup(table: dict) -> dict[str, str]:
147
+ return {variant.lower(): key for key, variants in table.items() for variant in variants}
148
+
149
+
150
+ _SECTION_LOOKUP = _lookup(SECTIONS)
151
+ _STEP_LOOKUP = _lookup(STEP_KEYS)
152
+ _KIND_LOOKUP = {**_lookup(REQ_KINDS), **KIND_ALIASES}
153
+ _REQ_LOOKUP = _lookup(REQ_KEYS)
154
+ _MISSING_LOOKUP = {**_lookup(MISSING), **{alias.lower(): key for alias, key in MISSING_ALIASES.items()}}
155
+
156
+
157
+ def section_key(heading: str) -> str | None:
158
+ return _SECTION_LOOKUP.get(heading.strip().lower())
159
+
160
+
161
+ def step_key(label: str) -> str | None:
162
+ return _STEP_LOOKUP.get(label.strip().lower())
163
+
164
+
165
+ def kind_key(label: str) -> str | None:
166
+ return _KIND_LOOKUP.get(label.strip().lower())
167
+
168
+
169
+ def req_key(label: str) -> str | None:
170
+ return _REQ_LOOKUP.get(label.strip().lower())
171
+
172
+
173
+ def missing_value(label: str) -> str | None:
174
+ return _MISSING_LOOKUP.get(label.strip().lower())
175
+
176
+
177
+ def section_text(sections: dict[str, str], key: str) -> str:
178
+ """The body of a logical section, whichever language the file spells its heading in."""
179
+ for variant in SECTIONS[key]:
180
+ if variant in sections:
181
+ return sections[variant]
182
+ for heading, body in sections.items():
183
+ if section_key(heading) == key:
184
+ return body
185
+ return ""
186
+
187
+
188
+ def detect(body: str, default: str = "zh") -> str:
189
+ """The language this SOP body is written in, judged by its section headings."""
190
+ counts = {language: 0 for language in LANGUAGES}
191
+ for line in body.splitlines():
192
+ match = re.match(r"^##\s+(.+?)\s*$", line)
193
+ if match:
194
+ for index, language in enumerate(LANGUAGES):
195
+ counts[language] += any(variants[index] == match.group(1) for variants in SECTIONS.values())
196
+ if counts["zh"] == counts["en"]:
197
+ return default
198
+ return "zh" if counts["zh"] > counts["en"] else "en"
199
+
200
+
201
+ def detect_case(body: str, default: str) -> str:
202
+ """The language of a SOP run Case, judged by its checklist heading."""
203
+ for index, language in enumerate(LANGUAGES):
204
+ if f"## {CASE_SECTIONS['checklist'][index]}\n" in body or body.rstrip().endswith(f"## {CASE_SECTIONS['checklist'][index]}"):
205
+ return language
206
+ return default
207
+
208
+
209
+ def checklist_headings() -> tuple[str, ...]:
210
+ return CASE_SECTIONS["checklist"]
211
+
212
+
213
+ def prefixes(key: str) -> tuple[str, ...]:
214
+ return TEXTS[key]
215
+
216
+
217
+ def strip_tools(step_text: str) -> str:
218
+ return _TOOLS_SPLIT.split(step_text)[0]
219
+
220
+
221
+ def is_placeholder_free(value: str) -> bool:
222
+ return not any(marker in value for marker in PLACEHOLDERS)
223
+
224
+
225
+ def count_placeholders(value: str) -> int:
226
+ return sum(value.count(marker) for marker in PLACEHOLDERS)
@@ -1,19 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """wf:人用的文件查找入口,基于交付物台账(runtime/assets.py),无需 Agent。
3
-
4
- wf 交互模式
5
- wf 笔记 9月1号 直接查(日期、同义词自动识别)
6
- wf -o 笔记 9月1号 查到后直接打开最新一个
7
- wf recent 最近改动的交付物
8
- wf cases [词] Case 列表(进行中在前)
9
- wf refs <路径|序号> [层数] 谁引用它 / 它引用谁
10
- wf o|f|c|p|v <序号> 打开 / 在文件管理器中显示 / 复制路径 / 打印路径 / 列出版本(序号指上一次结果)
11
- wf check 台账巡检摘要
12
- wf rebuild 强制重建台账
13
-
14
- --project <目录> 与 --output json 可写在任意位置;缺省从当前目录向上查找 Project Profile。
15
- 打开、显示与复制只在 macOS 上调用系统命令,其他平台打印绝对路径。
16
- """
2
+ """wf:人用的文件查找入口,基于交付物台账(runtime/assets.py),无需 Agent。"""
17
3
  from __future__ import annotations
18
4
 
19
5
  import json
@@ -28,10 +14,27 @@ from pathlib import Path
28
14
  sys.path.insert(0, str(Path(__file__).resolve().parent))
29
15
  import assets # noqa: E402
30
16
  from assets import AssetError, Project # noqa: E402
17
+ from i18n_orchestrator import _, N_ # noqa: E402
18
+
19
+ USAGE = N_("""wf:人用的文件查找入口,基于交付物台账(runtime/assets.py),无需 Agent。
20
+
21
+ wf 交互模式
22
+ wf 笔记 9月1号 直接查(日期、同义词自动识别)
23
+ wf -o 笔记 9月1号 查到后直接打开最新一个
24
+ wf recent 最近改动的交付物
25
+ wf cases [词] Case 列表(进行中在前)
26
+ wf refs <路径|序号> [层数] 谁引用它 / 它引用谁
27
+ wf o|f|c|p|v <序号> 打开 / 在文件管理器中显示 / 复制路径 / 打印路径 / 列出版本(序号指上一次结果)
28
+ wf check 台账巡检摘要
29
+ wf rebuild 强制重建台账
30
+
31
+ --project <目录> 与 --output json 可写在任意位置;缺省从当前目录向上查找 Project Profile。
32
+ 打开、显示与复制只在 macOS 上调用系统命令,其他平台打印绝对路径。
33
+ """)
31
34
 
32
35
  ACTIONS = {"o": "open", "f": "reveal", "c": "copy", "v": "versions", "r": "refs", "p": "path"}
33
- STATUS_LABEL = {"active": "进行中", "waiting": "等待", "done": "已完成", "captured": "待澄清",
34
- "suspended": "挂起", "abandoned": "放弃"}
36
+ STATUS_LABEL = {"active": N_("进行中"), "waiting": N_("等待"), "done": N_("已完成"), "captured": N_("待澄清"),
37
+ "suspended": N_("挂起"), "abandoned": N_("放弃")}
35
38
 
36
39
 
37
40
  # ---------------------------------------------------------------- display
@@ -90,7 +93,7 @@ def status_text(item: dict) -> str:
90
93
  status = item.get("status", "")
91
94
  if not status:
92
95
  return "project" if item.get("container_kind") == "project" else "-"
93
- label = STATUS_LABEL.get(status, status)
96
+ label = _(STATUS_LABEL.get(status, status))
94
97
  return S.green(label) if status == "active" else S.yellow(label) if status == "waiting" else S.dim(label)
95
98
 
96
99
 
@@ -109,8 +112,8 @@ def render_groups(groups: list[dict]) -> None:
109
112
  print(S.dim(f" {clip(item['path'], width - 6)}"))
110
113
  if item.get("versions"):
111
114
  names = " · ".join(version["name"] for version in item["versions"][:4])
112
- more = f" 等 {len(item['versions'])} 个" if len(item["versions"]) > 4 else ""
113
- print(S.dim(f" └ 同系列另有 {len(item['versions'])} 个:{clip(names, width - 30)}{more}"))
115
+ more = _(" 等 {0} 个").format(len(item['versions'])) if len(item["versions"]) > 4 else ""
116
+ print(S.dim(_(" └ 同系列另有 {0} 个:{1}{2}").format(len(item['versions']), clip(names, width - 30), more)))
114
117
 
115
118
 
116
119
  # ---------------------------------------------------------------- last results
@@ -141,7 +144,7 @@ def resolve_target(token: str, last: list[dict]) -> dict | None:
141
144
  index = int(token) - 1
142
145
  if 0 <= index < len(last):
143
146
  return last[index]
144
- print(S.red(f"没有第 {token} 项(上一次结果共 {len(last)} 项)"))
147
+ print(S.red(_("没有第 {0} 项(上一次结果共 {1} 项)").format(token, len(last))))
145
148
  return None
146
149
  return {"path": token, "name": Path(token).name, "title": "", "modified": "", "versions": []}
147
150
 
@@ -154,13 +157,13 @@ def do_action(action: str, relative: str, project: Project, platform: str | None
154
157
  mac = (platform or sys.platform) == "darwin"
155
158
  if action == "open" and mac and shutil.which("open"):
156
159
  subprocess.run(["open", target], check=False)
157
- print(S.green("已打开 ") + relative)
160
+ print(S.green(_("已打开 ")) + relative)
158
161
  elif action == "reveal" and mac and shutil.which("open"):
159
162
  subprocess.run(["open", "-R", target], check=False)
160
- print(S.green("已在 Finder 中显示 ") + relative)
163
+ print(S.green(_("已在 Finder 中显示 ")) + relative)
161
164
  elif action == "copy" and mac and shutil.which("pbcopy"):
162
165
  subprocess.run(["pbcopy"], input=target.encode("utf-8"), check=False)
163
- print(S.green("已复制绝对路径 ") + target)
166
+ print(S.green(_("已复制绝对路径 ")) + target)
164
167
  else:
165
168
  print(target)
166
169
  return "printed"
@@ -169,7 +172,7 @@ def do_action(action: str, relative: str, project: Project, platform: str | None
169
172
 
170
173
  def show_versions(group: dict) -> None:
171
174
  print(S.bold(group.get("title") or group.get("name", "")))
172
- print(f" {(group.get('modified') or '')[:16].replace('T', ' ')} {group['path']} {S.green('最新')}")
175
+ print(f" {(group.get('modified') or '')[:16].replace('T', ' ')} {group['path']} {S.green(_('最新'))}")
173
176
  for version in group.get("versions", []):
174
177
  print(f" {(version.get('modified') or '')[:16].replace('T', ' ')} {version['path']} "
175
178
  f"{S.dim(version.get('git', ''))}")
@@ -179,38 +182,38 @@ def show_refs(project: Project, path: str, depth: int = 1) -> dict:
179
182
  result = assets.refs(project, path, depth=depth)
180
183
  print(S.bold(result["path"]))
181
184
  if result["inbound"]:
182
- print(S.cyan(" 被引用:"))
185
+ print(S.cyan(_(" 被引用:")))
183
186
  for item in result["inbound"][:25]:
184
- via = "" if item["via"] == "file" else S.dim("(目录级)")
187
+ via = "" if item["via"] == "file" else S.dim(_("(目录级)"))
185
188
  print(f" ← {item['src']}{via}")
186
189
  else:
187
- print(S.dim(" 没有被任何 Case 或扫描根里的文件引用"))
190
+ print(S.dim(_(" 没有被任何 Case 或扫描根里的文件引用")))
188
191
  if result["outbound"]:
189
- print(S.cyan(" 引用了:"))
192
+ print(S.cyan(_(" 引用了:")))
190
193
  for item in result["outbound"][:25]:
191
194
  print(f" → {item['dst']}")
192
195
  if result["affected_containers"]:
193
- print(S.yellow(" 改动会波及:") + "、".join(result["affected_containers"][:8]))
196
+ print(S.yellow(_(" 改动会波及:")) + _("、").join(result["affected_containers"][:8]))
194
197
  return result
195
198
 
196
199
 
197
200
  def do_search(project: Project, terms: list[str], *, limit: int = 10, **filters: str) -> list[dict]:
198
201
  if assets.status(project)["status"] != "fresh":
199
- print(S.dim("(索引更新中…)"))
202
+ print(S.dim(_("(索引更新中…)")))
200
203
  result = assets.find(project, terms, limit=limit, **filters)
201
204
  query = result["query"]
202
- description = " ".join(query["terms"]) or "全部"
205
+ description = " ".join(query["terms"]) or _("全部")
203
206
  if query["dates"]:
204
- description += " · 日期 " + ",".join(date.replace("*-", "") for date in query["dates"])
205
- print(S.dim(f"查找:{description}"))
207
+ description += _(" · 日期 ") + ",".join(date.replace("*-", "") for date in query["dates"])
208
+ print(S.dim(_("查找:{0}").format(description)))
206
209
  if not result["results"]:
207
- print(S.yellow("没有找到。") + S.dim("试试更短的词、去掉日期,或 `wf cases` 先看 Case 列表。"))
210
+ print(S.yellow(_("没有找到。")) + S.dim(_("试试更短的词、去掉日期,或 `wf cases` 先看 Case 列表。")))
208
211
  return []
209
212
  render_groups(result["results"])
210
- tail = f"共 {result['total']} 组"
213
+ tail = _("共 {0} 组").format(result['total'])
211
214
  if result["total"] > result["returned"]:
212
- tail += f",只显示前 {result['returned']} 组(加关键词收窄)"
213
- print(S.dim(tail + "。序号可接 o 打开 / f 显示 / c 复制 / v 版本 / r 引用,例:o1"))
215
+ tail += _(",只显示前 {0} 组(加关键词收窄)").format(result['returned'])
216
+ print(S.dim(tail + _("。序号可接 o 打开 / f 显示 / c 复制 / v 版本 / r 引用,例:o1")))
214
217
  save_last(project, result["results"])
215
218
  return result["results"]
216
219
 
@@ -219,11 +222,11 @@ def do_cases(project: Project, term: str | None, limit: int = 30) -> dict:
219
222
  result = assets.cases(project, term, limit=limit)
220
223
  width = term_width()
221
224
  for item in result["results"]:
222
- status = STATUS_LABEL.get(item["status"], item["status"] or "-")
225
+ status = _(STATUS_LABEL.get(item["status"], item["status"] or "-"))
223
226
  mark = S.green(f"{status:<4}") if item["status"] == "active" else S.dim(f"{status:<4}")
224
227
  print(f" {mark} {S.bold(clip(item['title'] or item['id'], width - 52))} {S.dim(clip(item['id'], 44))} "
225
- f"{S.dim(str(item['files']) + ' 个文件')}")
226
- print(S.dim(f"共 {result['total']} 个 Case" + (";用 `wf <Case 关键词>` 查其文件" if result["total"] else "")))
228
+ f"{S.dim(str(item['files']) + _(' 个文件'))}")
229
+ print(S.dim(_("共 {0} 个 Case").format(result['total']) + (_(";用 `wf <Case 关键词>` 查其文件") if result["total"] else "")))
227
230
  return result
228
231
 
229
232
 
@@ -238,26 +241,28 @@ def do_check(project: Project) -> dict:
238
241
  report = assets.check(project)
239
242
  totals, tiers, counts = report["totals"], report["claim_tiers"], report["broken_counts"]
240
243
  git = totals["git"]
241
- print(S.bold("台账巡检"))
242
- print(f" 文件 {totals['files']} · 引用 {totals['edges']} · Git 已入库 {git.get('tracked', 0)} / "
243
- f"不入库 {git.get('ignored', 0)} / 新建未提交 {git.get('untracked', 0)} / 非 Git {git.get('none', 0)}")
244
- print(f" 交付物:文件级被引用 {tiers.get('file', 0)} · 仅目录覆盖 {tiers.get('directory', 0)} · "
245
- f"无人认领 {S.yellow(str(tiers.get('none', 0)))}")
246
- print(f" 断链:真断链 {S.yellow(str(counts.get('real-broken', 0)))} · "
247
- f"说明文字 {counts.get('prose-not-path', 0)} · 主题链接 {counts.get('wikilink-topic', 0)}")
244
+ print(S.bold(_("台账巡检")))
245
+ print(_(" 文件 {0} · 引用 {1} · Git 已入库 {2} / 不入库 {3} / 新建未提交 {4} / 非 Git {5}").format(totals['files'], totals['edges'], git.get('tracked', 0), git.get('ignored', 0), git.get('untracked', 0), git.get('none', 0)))
246
+ print(_(" 交付物:文件级被引用 {0} · 仅目录覆盖 {1} · 无人认领 {2}").format(tiers.get('file', 0), tiers.get('directory', 0), S.yellow(str(tiers.get('none', 0)))))
247
+ print(_(" 断链:真断链 {0} · 说明文字 {1} · 主题链接 {2}").format(S.yellow(str(counts.get('real-broken', 0))), counts.get('prose-not-path', 0), counts.get('wikilink-topic', 0)))
248
248
  for container, paths in list(report["unclaimed"].items())[:5]:
249
- print(S.dim(f" 无人认领 · {container}: {paths[0]}"))
249
+ print(S.dim(_(" 无人认领 · {0}: {1}").format(container, paths[0])))
250
250
  return report
251
251
 
252
252
 
253
253
  # ---------------------------------------------------------------- entry
254
254
 
255
- HELP = f"""{S.bold('用法')}
256
- 直接输入关键词查找;可带日期(9月1号 / 0901 / 2026-09-01),同义词按项目的 asset-synonyms.json 展开。
257
- {S.cyan('o1')} 打开第 1 项 {S.cyan('f1')} 在文件管理器中显示 {S.cyan('c1')} 复制绝对路径 {S.cyan('p1')} 打印路径
258
- {S.cyan('v1')} 查看全部版本 {S.cyan('r1')} 谁引用它(r1 2 为两层) {S.cyan('1')} 直接打开
259
- {S.cyan('recent')} 最近改动 {S.cyan('cases [词]')} Case 列表 {S.cyan('check')} 巡检 {S.cyan('q')} 退出
260
- 过滤:{S.cyan('ext:xlsx')} {S.cyan('kind:deliverable')}(也可 draft/source/data) {S.cyan('in:片段')}(限定 Case/目录)"""
255
+ def help_text() -> str:
256
+ cyan = S.cyan
257
+ return (S.bold(_("用法")) + "\n"
258
+ + _(" 直接输入关键词查找;可带日期(9月1号 / 0901 / 2026-09-01),同义词按项目的 asset-synonyms.json 展开。") + "\n"
259
+ + _(" {0} 打开第 1 项 {1} 在文件管理器中显示 {2} 复制绝对路径 {3} 打印路径").format(
260
+ cyan("o1"), cyan("f1"), cyan("c1"), cyan("p1")) + "\n"
261
+ + _(" {0} 查看全部版本 {1} 谁引用它(r1 2 为两层) {2} 直接打开").format(cyan("v1"), cyan("r1"), cyan("1")) + "\n"
262
+ + _(" {0} 最近改动 {1} Case 列表 {2} 巡检 {3} 退出").format(
263
+ cyan("recent"), cyan("cases [词]"), cyan("check"), cyan("q")) + "\n"
264
+ + _(" 过滤:{0} {1}(也可 draft/source/data) {2}(限定 Case/目录)").format(
265
+ cyan("ext:xlsx"), cyan("kind:deliverable"), cyan("in:片段")))
261
266
 
262
267
 
263
268
  def parse_filters(tokens: list[str]) -> tuple[list[str], dict]:
@@ -277,7 +282,7 @@ def split_globals(argv: list[str]) -> tuple[list[str], Path | None, str]:
277
282
  index = 0
278
283
  while index < len(argv):
279
284
  token = argv[index]
280
- name, _, inline = token.partition("=")
285
+ name, _sep, inline = token.partition("=")
281
286
  if name in ("--project", "--output"):
282
287
  if inline:
283
288
  value = inline
@@ -285,13 +290,13 @@ def split_globals(argv: list[str]) -> tuple[list[str], Path | None, str]:
285
290
  index += 1
286
291
  value = argv[index]
287
292
  else:
288
- raise AssetError(f"{name} 需要一个值")
293
+ raise AssetError(_("{0} 需要一个值").format(name))
289
294
  if name == "--project":
290
295
  project = Path(value)
291
296
  elif value in ("text", "json"):
292
297
  output = value
293
298
  else:
294
- raise AssetError("--output 只能是 text 或 json")
299
+ raise AssetError(_("--output 只能是 text 或 json"))
295
300
  else:
296
301
  rest.append(token)
297
302
  index += 1
@@ -305,16 +310,15 @@ def interactive(project: Project) -> int:
305
310
  pass
306
311
  current = assets.status(project)
307
312
  if current["status"] != "fresh":
308
- print(S.dim("(索引更新中…)"))
313
+ print(S.dim(_("(索引更新中…)")))
309
314
  assets.rebuild(project)
310
315
  current = assets.status(project)
311
- print(S.bold(S.cyan("交付物台账")) + S.dim(f" · {current.get('file_count', '?')} 个文件 · 索引 "
312
- f"{current.get('built_at', '')[11:16]}"))
313
- print(S.dim("输入关键词查找;? 帮助;q 退出"))
316
+ print(S.bold(S.cyan(_("交付物台账"))) + S.dim(_(" · {0} 个文件 · 索引 {1}").format(current.get('file_count', '?'), current.get('built_at', '')[11:16])))
317
+ print(S.dim(_("输入关键词查找;? 帮助;q 退出")))
314
318
  groups: list[dict] = []
315
319
  while True:
316
320
  try:
317
- line = input(S.bold(S.cyan("\n查找 › "))).strip()
321
+ line = input(S.bold(S.cyan(_("\n查找 › ")))).strip()
318
322
  except (EOFError, KeyboardInterrupt):
319
323
  print()
320
324
  return 0
@@ -323,7 +327,7 @@ def interactive(project: Project) -> int:
323
327
  if line in ("q", "quit", "exit", "退出"):
324
328
  return 0
325
329
  if line in ("?", "help", "帮助"):
326
- print(HELP)
330
+ print(help_text())
327
331
  continue
328
332
  try:
329
333
  match = re.match(r"^([ofcvrp])\s*(\d+)(?:\s+(\d+))?$", line) or re.match(r"^()(\d+)()$", line)
@@ -351,7 +355,7 @@ def interactive(project: Project) -> int:
351
355
  terms, filters = parse_filters(line.split())
352
356
  groups = do_search(project, terms, **filters)
353
357
  except AssetError as exc:
354
- print(S.red(f"出错:{exc}"))
358
+ print(S.red(_("出错:{0}").format(exc)))
355
359
 
356
360
 
357
361
  def command(project: Project, argv: list[str], output: str) -> tuple[int, dict | None]:
@@ -387,7 +391,7 @@ def command(project: Project, argv: list[str], output: str) -> tuple[int, dict |
387
391
  return 0, None
388
392
  if head == "rebuild":
389
393
  result = assets.rebuild(project, force=True)
390
- print(f"{result['action']}:{result['file_count']} 个文件")
394
+ print(_("{0}:{1} 个文件").format(result['action'], result['file_count']))
391
395
  return 0, None
392
396
  if (head in ACTIONS and len(argv) > 1 and (argv[1].isdigit() or head == "r")) or (head == "refs" and len(argv) > 1):
393
397
  target = resolve_target(argv[1], load_last(project))
@@ -415,23 +419,23 @@ def main(argv: list[str] | None = None) -> int:
415
419
  try:
416
420
  argv, start, output = split_globals(argv)
417
421
  if argv and argv[0] in ("-h", "--help", "help"):
418
- print(__doc__)
422
+ print(_(USAGE))
419
423
  return 0
420
424
  project = Project.discover(start, explicit=start is not None)
421
425
  if not argv:
422
426
  if output == "json":
423
- raise AssetError("交互模式不支持 --output json")
427
+ raise AssetError(_("交互模式不支持 --output json"))
424
428
  return interactive(project)
425
429
  code, result = command(project, argv, output)
426
430
  if output == "json":
427
431
  print(json.dumps(result if result is not None else {"ok": False, "code": "not-found",
428
- "error": "没有这一项"}, ensure_ascii=False, indent=2))
432
+ "error": _("没有这一项")}, ensure_ascii=False, indent=2))
429
433
  return code
430
434
  except AssetError as exc:
431
435
  if output == "json":
432
436
  print(json.dumps({"ok": False, "code": exc.code, "error": str(exc)}, ensure_ascii=False))
433
437
  else:
434
- print(S.red(f"出错:{exc}"), file=sys.stderr)
438
+ print(S.red(_("出错:{0}").format(exc)), file=sys.stderr)
435
439
  return 2
436
440
 
437
441
 
@@ -116,6 +116,28 @@ Case 自有输入放 `refs/`,交付物放 `docs/`(草稿 `docs/drafts/`)
116
116
 
117
117
  Project Profile 声明了 `memory` 时,恢复状态后读取记忆索引:`provider` 为 `project-orchestrator` 时运行随包的 `runtime/memory.py index --project <项目>`,为 `project` 时运行其 `index_command`。索引每条一行,说明何时应当想起该条记忆;只在当前任务命中时读取条目正文。依据某条记忆行动前,核对其中提到的文件、命令与外部对象仍然存在;标注为可能过期的条目先核实再使用。
118
118
 
119
+ ### 3.1 流程(SOP)
120
+
121
+ 用户可以把常用流程固化成 SOP(Standard Operating Procedure,标准作业流程)。契约与命令见 [SOP 契约](../../references/sop.md),与用户一起建立见 [建立 SOP](../../references/sop-interview.md)。
122
+
123
+ - **读索引**:存在 `sops/` 目录或用户级 SOP 时,恢复状态后运行随包的 `runtime/sop.py index --project <项目>`(一行一个已启用的 SOP,写明触发与不触发条件)。没有 SOP 时静默跳过。
124
+ - **判断触发**:用户明确指定、事件(新建或关闭 Case 时,`case.py` 与项目入口的结果带 `sop_events`;自有 Case 工具用 `sop.py events <事件> --case <Case>` 查询)、周期到期(`sop.py due`)、意图对照触发与不触发条件,按这个优先级。命中后默认先问用户一次是否按它执行;多个命中合并成一次编号提问;到期只是提醒。只读被命中的那一个 SOP 的正文。
125
+ - **执行**:`sop.py preflight` 检查所需程序与环境,缺失项一次性列给用户并等待处理,**不自行安装软件**;`sop.py run`(预览 → 用户确认 → `--apply`)建立执行 Case;逐步执行并用 `sop.py step --apply` 记录结果与中间产物;出错、缺工具、花钱或耗时步骤按 Case 里固化的“执行约定”处理;收尾用 `sop.py status` 与完成标准核对。
126
+ - **范围与私有目录**:会读取多个事项的流程(复盘、汇总)先让用户指定范围,不默认纳入全部事项;SOP 里用到私有目录(见 3.2)时,`sop.py run` 自动把执行 Case 标为 `private`,读取前按 3.2 取得用户确认。
127
+ - **授权不变**:SOP 不放宽任何授权,写入、外发、删除、push、安装、花钱类步骤仍须用户当次同意;与用户当次请求冲突时以请求为准并说明偏离。
128
+ - **创建与修改**:SOP 里的工具、出错处理、中间产物位置等是用户的习惯,只提问、整理、校验,不替用户决定;`decided` 只在用户确认后用 `sop.py decide` 写入;修改后用 `sop.py revise` 登记版本。
129
+ - 一次执行的状态只写在 Case 里,不在 SOP 目录里另记;SOP 不是 Opinion(表达偏好)也不是记忆(事实),不要写错位置。
130
+
131
+ ### 3.2 私有目录
132
+
133
+ Profile 的 `privacy.forbidden_default_reads` 声明私有路径(没有声明但项目根有 `private/` 时,按约定它就是私有目录)。三条约定,其余照常:
134
+
135
+ - **读取前须经用户确认**:默认不读、不列目录、不搜索其中的内容。需要读时,先向用户列出要读的路径和用途,得到确认再读;确认后用 `sop.py confirm-read <Case> <路径…> --apply` 记入该 Case 的决策记录(同时把 Case 标为 `private`)。确认只覆盖当次列出的路径。
136
+ - **内容默认不进入 git**:私有目录里有 `.gitignore`(`*` 加 `!.gitignore`)。`sop.py private` 查看状态,`--apply` 补上缺失的 `.gitignore`;`health` 会提示未被忽略的私有目录和已被跟踪的文件。不要替用户 `git add`、提交或推送私有目录里的内容。
137
+ - **用到私有目录的 Case 标为 `sensitivity: private`**:`case.py update --add-deliverable/--add-evidence` 登记私有路径时自动改,也可 `--sensitivity private`;SOP 里引用了私有路径时 `sop.py run` 自动标。云同步盘同步它没有问题。
138
+
139
+ 这是协议级约束:工具层挡不住 Agent 直接读文件,所以要靠这里的规则;`find` 照常索引 `private` 的 Case(索引只是本机缓存里的索引,不做过滤)。
140
+
119
141
  ## 4. 路由能力
120
142
 
121
143
  根据三项输入构建执行路由:
@@ -0,0 +1,57 @@
1
+ ---
2
+ schema_version: "1.0"
3
+ id: "{{id}}"
4
+ title: "{{title}}"
5
+ status: "draft"
6
+ version: "1.0.0"
7
+ description: "[TO FILL IN] One sentence: what this procedure does and when to think of it"
8
+ trigger: "[TO FILL IN] When this procedure should be used"
9
+ exclude: ""
10
+ confirm: "ask"
11
+ creates_case: "always"
12
+ created: "{{date}}"
13
+ aliases: []
14
+ examples_hit: []
15
+ examples_miss: []
16
+ on_events: []
17
+ outputs: []
18
+ sources: []
19
+ decided: []
20
+ ---
21
+ # {{title}}
22
+
23
+ ## Purpose
24
+
25
+ [TO FILL IN] What this procedure should achieve, in a sentence or two.
26
+
27
+ ## Prerequisites
28
+
29
+ One item per line: `- Kind: name | Purpose: … | Install: … | Alternative: name of another item | If missing: Ask the user`.
30
+ Kinds: Command, Module (a Python module), Environment variable, Path, Skill, MCP, Manual check. If missing: Ask the user, Stop, Skip the step, Use the alternative.
31
+ For API keys write only the environment variable name, never the key itself. If no tool is needed at all, write "None".
32
+
33
+ - Command: [TO FILL IN] | Purpose: [TO FILL IN] | Install: [TO FILL IN] | If missing: Ask the user
34
+
35
+ ## Steps
36
+
37
+ One number per step, with indented sub-items below it. Sub-items: Tools, Inputs, Intermediates, Final outputs, Checkpoint, On failure, Needs confirmation, Note.
38
+ Every name under "Tools" must be declared in the prerequisites above.
39
+
40
+ 1. [TO FILL IN] What the first step does
41
+ - Tools: [TO FILL IN]
42
+ - Inputs: [TO FILL IN]
43
+ - Intermediates: [TO FILL IN]
44
+ - Checkpoint: [TO FILL IN] How to tell the step went right
45
+ - On failure: [TO FILL IN] (if left out, the "run conventions" apply)
46
+
47
+ ## Completion criteria
48
+
49
+ - [TO FILL IN] How to tell the whole procedure is done (a condition that can be checked, for example "the output file exists at …")
50
+
51
+ ## Exceptions and fallback
52
+
53
+ - [TO FILL IN] Common exceptions and how to handle them; write "None" if there are none.
54
+
55
+ ## Revision log
56
+
57
+ None yet.