@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
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
  from typing import Any
12
12
 
13
13
  from constants import KNOWN_PLACEHOLDERS
14
+ from i18n_opinion import _ # noqa: E402
14
15
 
15
16
 
16
17
  PUBLIC_CATALOG = Path(__file__).resolve().parents[1] / "catalog"
@@ -53,74 +54,74 @@ def safe_path(path: Path) -> Path:
53
54
  path = Path(os.path.abspath(path.expanduser()))
54
55
  for component in (path, *path.parents):
55
56
  if component.is_symlink():
56
- fail(f"拒绝使用符号链接:{component}")
57
+ fail(_("拒绝使用符号链接:{0}").format(component))
57
58
  return path
58
59
 
59
60
 
60
61
  def read_text(path: Path) -> str:
61
62
  path = safe_path(path)
62
63
  if not stat.S_ISREG(path.lstat().st_mode):
63
- fail(f"必须使用普通文件:{path}")
64
+ fail(_("必须使用普通文件:{0}").format(path))
64
65
  return path.read_text(encoding="utf-8")
65
66
 
66
67
 
67
68
  def read_object(path: Path) -> dict[str, Any]:
68
69
  value = json.loads(read_text(path))
69
70
  if not isinstance(value, dict):
70
- fail(f"JSON 根节点必须是对象:{path}")
71
+ fail(_("JSON 根节点必须是对象:{0}").format(path))
71
72
  return value
72
73
 
73
74
 
74
75
  def fields(value: Any, expected: set[str], label: str) -> None:
75
76
  if not isinstance(value, dict) or set(value) != expected:
76
- fail(f"{label} 字段必须为:{sorted(expected)}")
77
+ fail(_("{0} 字段必须为:{1}").format(label, sorted(expected)))
77
78
 
78
79
 
79
80
  def string(value: Any, label: str) -> None:
80
81
  if not isinstance(value, str) or not value.strip():
81
- fail(f"{label} 必须是非空字符串")
82
+ fail(_("{0} 必须是非空字符串").format(label))
82
83
 
83
84
 
84
85
  def identifier(value: Any) -> None:
85
86
  if not isinstance(value, str) or not IDENTIFIER.fullmatch(value):
86
- fail(f"编号无效:{value!r}")
87
+ fail(_("编号无效:{0!r}").format(value))
87
88
 
88
89
 
89
90
  def version(value: Any) -> tuple[int, int, int]:
90
91
  if not isinstance(value, str) or not VERSION.fullmatch(value):
91
- fail(f"版本必须是无前导零的三段数字:{value!r}")
92
+ fail(_("版本必须是无前导零的三段数字:{0!r}").format(value))
92
93
  return tuple(int(part) for part in value.split("."))
93
94
 
94
95
 
95
96
  def strings(value: Any, label: str, *, empty: bool = False) -> None:
96
97
  if not isinstance(value, list) or (not empty and not value):
97
- fail(f"{label} 必须是字符串数组")
98
+ fail(_("{0} 必须是字符串数组").format(label))
98
99
  for item in value:
99
100
  string(item, label)
100
101
  if len(value) != len(set(value)):
101
- fail(f"{label} 包含重复项")
102
+ fail(_("{0} 包含重复项").format(label))
102
103
 
103
104
 
104
105
  def parse_ref(ref: str, kind: str) -> tuple[str, str]:
105
106
  if not isinstance(ref, str) or ref.count("@") != 1:
106
- fail(f"{kind} 必须精确指定 @VERSION:{ref!r}")
107
+ fail(_("{0} 必须精确指定 @VERSION:{1!r}").format(kind, ref))
107
108
  identity, release = ref.split("@")
108
109
  version(release)
109
110
  parts = identity.split("/")
110
111
  if len(parts) != (2 if kind == "templates" else 1):
111
- fail(f"{kind} 引用格式无效:{ref}")
112
+ fail(_("{0} 引用格式无效:{1}").format(kind, ref))
112
113
  for part in parts:
113
114
  identifier(part)
114
115
  return identity, release
115
116
 
116
117
 
117
118
  def validate_edit(value: Any) -> None:
118
- fields(value, EDITABLE, "规则内容")
119
+ fields(value, EDITABLE, _("规则内容"))
119
120
  string(value["title"], "title")
120
121
  string(value["text"], "text")
121
122
  strings(value["scopes"], "scopes")
122
123
  if value["level"] not in ("required", "preferred"):
123
- fail("level 必须为 required 或 preferred")
124
+ fail(_("level 必须为 required 或 preferred"))
124
125
 
125
126
 
126
127
  def release_ref(value: dict[str, Any], kind: str) -> str:
@@ -130,9 +131,9 @@ def release_ref(value: dict[str, Any], kind: str) -> str:
130
131
 
131
132
  def validate_release(value: Any) -> str:
132
133
  if not isinstance(value, dict):
133
- fail("发布内容必须是对象")
134
+ fail(_("发布内容必须是对象"))
134
135
  kind = "rules" if "id" in value else "templates"
135
- fields(value, RULE_FIELDS if kind == "rules" else TEMPLATE_FIELDS, "发布内容")
136
+ fields(value, RULE_FIELDS if kind == "rules" else TEMPLATE_FIELDS, _("发布内容"))
136
137
  version(value["version"])
137
138
  if kind == "rules":
138
139
  identifier(value["id"])
@@ -158,7 +159,7 @@ def release_path(root: Path, value: dict[str, Any], kind: str) -> Path:
158
159
  def load_catalog(root: Path) -> dict[str, dict[str, Any]]:
159
160
  root = safe_path(root)
160
161
  if not root.is_dir():
161
- fail(f"目录不存在:{root}")
162
+ fail(_("目录不存在:{0}").format(root))
162
163
  catalog: dict[str, dict[str, Any]] = {"templates": {}, "rules": {}}
163
164
  for path in sorted(root.rglob("*")):
164
165
  safe_path(path)
@@ -167,7 +168,7 @@ def load_catalog(root: Path) -> dict[str, dict[str, Any]]:
167
168
  value = read_object(path)
168
169
  kind = validate_release(value)
169
170
  if path != release_path(root, value, kind):
170
- fail(f"发布文件路径与身份不一致:{path}")
171
+ fail(_("发布文件路径与身份不一致:{0}").format(path))
171
172
  ref = release_ref(value, kind)
172
173
  catalog[kind][ref] = value
173
174
  for template in catalog["templates"].values():
@@ -181,24 +182,24 @@ def resolve(catalog: dict[str, dict[str, Any]], templates: list[str], rules: lis
181
182
  sources: dict[str, list[Any]] = {"templates": [], "rules": []}
182
183
  references = list(rules)
183
184
  for ref in templates:
184
- identity, _ = parse_ref(ref, "templates")
185
+ identity, _version = parse_ref(ref, "templates")
185
186
  family = identity.split("/")[0]
186
187
  if family in families:
187
- fail(f"同一模板系列只能选择一个变体和版本:{family}")
188
+ fail(_("同一模板系列只能选择一个变体和版本:{0}").format(family))
188
189
  families.add(family)
189
190
  if ref not in catalog["templates"]:
190
- fail(f"未知模板:{ref}")
191
+ fail(_("未知模板:{0}").format(ref))
191
192
  value = catalog["templates"][ref]
192
193
  sources["templates"].append({"ref": ref, "sha256": digest(value), "value": value})
193
194
  references.extend(value["rules"])
194
195
  selected: dict[str, Any] = {}
195
196
  for ref in references:
196
- identity, _ = parse_ref(ref, "rules")
197
+ identity, _version = parse_ref(ref, "rules")
197
198
  if ref not in catalog["rules"]:
198
- fail(f"未知规则:{ref}")
199
+ fail(_("未知规则:{0}").format(ref))
199
200
  value = catalog["rules"][ref]
200
201
  if identity in selected and selected[identity]["ref"] != ref:
201
- fail(f"规则选择了不同版本:{identity}")
202
+ fail(_("规则选择了不同版本:{0}").format(identity))
202
203
  selected.setdefault(identity, {"ref": ref, "sha256": digest(value), "value": value})
203
204
  sources["rules"] = list(selected.values())
204
205
  return sources
@@ -219,7 +220,7 @@ def effective_rules(profile: dict[str, Any]) -> dict[str, Any]:
219
220
  for identity, rule in result.items():
220
221
  slot = rule["slot"]
221
222
  if slot in slots:
222
- fail(f"规则 slot 冲突:{slot}({slots[slot]}、{identity})")
223
+ fail(_("规则 slot 冲突:{0}({1}、{2})").format(slot, slots[slot], identity))
223
224
  slots[slot] = identity
224
225
  return result
225
226
 
@@ -233,45 +234,45 @@ def validate_loading(profile: dict[str, Any]) -> None:
233
234
  fields(loading, LOADING_FIELDS, "loading")
234
235
  strings(loading["core"], "loading.core", empty=True)
235
236
  if not isinstance(loading["signals"], dict) or not loading["signals"]:
236
- fail("loading.signals 必须是非空对象")
237
+ fail(_("loading.signals 必须是非空对象"))
237
238
  for key, values in loading["signals"].items():
238
239
  identifier(key)
239
240
  strings(values, f"loading.signals.{key}")
240
241
  for item in values:
241
242
  identifier(item)
242
243
  if not isinstance(loading["bundles"], list) or not loading["bundles"]:
243
- fail("loading.bundles 至少包含一个按需规则束")
244
+ fail(_("loading.bundles 至少包含一个按需规则束"))
244
245
  families = set(selected_families(profile))
245
246
  seen = set(loading["core"])
246
247
  for family in loading["core"]:
247
248
  if family not in families:
248
- fail(f"loading.core 引用了未选择的模板系列:{family}")
249
+ fail(_("loading.core 引用了未选择的模板系列:{0}").format(family))
249
250
  for bundle in loading["bundles"]:
250
- fields(bundle, BUNDLE_FIELDS, "loading.bundles 项")
251
+ fields(bundle, BUNDLE_FIELDS, _("loading.bundles 项"))
251
252
  family = bundle["family"]
252
253
  if family not in families:
253
- fail(f"loading.bundles 引用了未选择的模板系列:{family!r}")
254
+ fail(_("loading.bundles 引用了未选择的模板系列:{0!r}").format(family))
254
255
  if family in seen:
255
- fail(f"模板系列在 loading 中重复出现:{family}")
256
+ fail(_("模板系列在 loading 中重复出现:{0}").format(family))
256
257
  seen.add(family)
257
258
  string(bundle["trigger"], "trigger")
258
259
  string(bundle["exclude"], "exclude")
259
260
  if not isinstance(bundle["routes"], list) or not bundle["routes"]:
260
- fail(f"规则束至少需要一条路由:{family}")
261
+ fail(_("规则束至少需要一条路由:{0}").format(family))
261
262
  for route in bundle["routes"]:
262
263
  if not isinstance(route, dict) or not route:
263
- fail(f"路由必须是非空对象:{family}")
264
+ fail(_("路由必须是非空对象:{0}").format(family))
264
265
  for key, values in route.items():
265
266
  strings(values, f"routes.{key}")
266
267
  if key not in loading["signals"] or not set(values) <= set(loading["signals"][key]):
267
- fail(f"路由使用了未声明的信号:{family}.{key}")
268
+ fail(_("路由使用了未声明的信号:{0}.{1}").format(family, key))
268
269
 
269
270
 
270
271
  def validate_profile(value: Any) -> dict[str, Any]:
271
272
  tiered = isinstance(value, dict) and value.get("schema_version") == "2.1"
272
273
  fields(value, PROFILE_FIELDS | PROFILE_OPTIONAL_FIELDS if tiered else PROFILE_FIELDS, "Profile")
273
274
  if value["schema_version"] not in ("2.0", "2.1"):
274
- fail("Profile schema_version 必须为 2.0 或 2.1")
275
+ fail(_("Profile schema_version 必须为 2.0 或 2.1"))
275
276
  identifier(value["id"])
276
277
  version(value["version"])
277
278
  strings(value["templates"], "templates", empty=True)
@@ -280,27 +281,27 @@ def validate_profile(value: Any) -> dict[str, Any]:
280
281
  catalog: dict[str, dict[str, Any]] = {"templates": {}, "rules": {}}
281
282
  for kind, sources in value["sources"].items():
282
283
  if not isinstance(sources, list):
283
- fail("sources 项必须是数组")
284
+ fail(_("sources 项必须是数组"))
284
285
  for source in sources:
285
286
  fields(source, {"ref", "sha256", "value"}, "source")
286
287
  if validate_release(source["value"]) != kind:
287
- fail("source 类型不一致")
288
+ fail(_("source 类型不一致"))
288
289
  if source["ref"] != release_ref(source["value"], kind) or source["sha256"] != digest(source["value"]):
289
- fail(f"source 指纹或引用不一致:{source['ref']}")
290
+ fail(_("source 指纹或引用不一致:{0}").format(source['ref']))
290
291
  if source["ref"] in catalog[kind]:
291
- fail(f"重复 source:{source['ref']}")
292
+ fail(_("重复 source:{0}").format(source['ref']))
292
293
  catalog[kind][source["ref"]] = source["value"]
293
294
  if resolve(catalog, value["templates"], value["rules"]) != value["sources"]:
294
- fail("Profile 引用与 sources 不一致")
295
+ fail(_("Profile 引用与 sources 不一致"))
295
296
  if not isinstance(value["overrides"], dict):
296
- fail("overrides 必须是对象")
297
+ fail(_("overrides 必须是对象"))
297
298
  for identity, override in value["overrides"].items():
298
299
  identifier(identity)
299
300
  if override is not None:
300
301
  if validate_release(override) != "rules" or override["id"] != identity:
301
- fail("override 必须使用匹配编号的完整规则")
302
+ fail(_("override 必须使用匹配编号的完整规则"))
302
303
  elif identity not in base_rules(value):
303
- fail(f"停用规则不存在:{identity}")
304
+ fail(_("停用规则不存在:{0}").format(identity))
304
305
  if value["custom"] is not None:
305
306
  string(value["custom"], "custom")
306
307
  if value["derived_from"] is not None:
@@ -308,9 +309,9 @@ def validate_profile(value: Any) -> dict[str, Any]:
308
309
  identifier(value["derived_from"]["id"])
309
310
  version(value["derived_from"]["version"])
310
311
  if not isinstance(value["derived_from"]["sha256"], str) or not re.fullmatch(r"[0-9a-f]{64}", value["derived_from"]["sha256"]):
311
- fail("derived_from 指纹无效")
312
+ fail(_("derived_from 指纹无效"))
312
313
  if not effective_rules(value) and not value["custom"]:
313
- fail("Profile 至少需要一条有效规则或自定义内容")
314
+ fail(_("Profile 至少需要一条有效规则或自定义内容"))
314
315
  if tiered:
315
316
  validate_loading(value)
316
317
  return value
@@ -324,10 +325,10 @@ def replace_refs(previous: list[str], incoming: list[str], kind: str) -> list[st
324
325
  result = list(previous)
325
326
  keys: set[str] = set()
326
327
  for ref in incoming:
327
- identity, _ = parse_ref(ref, kind)
328
+ identity, _version = parse_ref(ref, kind)
328
329
  key = identity.split("/")[0] if kind == "templates" else identity
329
330
  if key in keys:
330
- fail(f"同一次选择包含重复身份:{key}")
331
+ fail(_("同一次选择包含重复身份:{0}").format(key))
331
332
  keys.add(key)
332
333
  result = [item for item in result if (parse_ref(item, kind)[0].split("/")[0] if kind == "templates" else parse_ref(item, kind)[0]) != key]
333
334
  result.append(ref)
@@ -367,7 +368,7 @@ def immutable_write(path: Path, value: dict[str, Any]) -> str:
367
368
  if path.exists():
368
369
  if read_object(path) == value:
369
370
  return "unchanged"
370
- fail(f"已经发布的版本不可修改:{path}")
371
+ fail(_("已经发布的版本不可修改:{0}").format(path))
371
372
  path.parent.mkdir(parents=True, exist_ok=True)
372
373
  descriptor, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
373
374
  temporary = Path(name)
@@ -381,35 +382,35 @@ def immutable_write(path: Path, value: dict[str, Any]) -> str:
381
382
  finally:
382
383
  temporary.unlink()
383
384
  if read_object(path) != value:
384
- fail(f"写入后核对失败:{path}")
385
+ fail(_("写入后核对失败:{0}").format(path))
385
386
  return "written"
386
387
 
387
388
 
388
389
  def confirm(args: argparse.Namespace, fingerprint: str) -> None:
389
390
  if args.confirm != fingerprint:
390
- fail(f"请先审阅预览,再使用 --confirm {fingerprint} --apply")
391
+ fail(_("请先审阅预览,再使用 --confirm {0} --apply").format(fingerprint))
391
392
 
392
393
 
393
394
  def profile_command(args: argparse.Namespace) -> dict[str, Any]:
394
395
  root = safe_path(args.project)
395
396
  if not root.is_dir():
396
- fail(f"项目目录不存在:{root}")
397
+ fail(_("项目目录不存在:{0}").format(root))
397
398
  identifier(args.id)
398
399
  version(args.version)
399
400
  old = load_profile(args.from_profile) if args.from_profile else None
400
401
  if old and args.id == old["id"] and version(args.version) <= version(old["version"]):
401
- fail("修订 Profile 必须增加版本;私有派生必须使用独立编号")
402
+ fail(_("修订 Profile 必须增加版本;私有派生必须使用独立编号"))
402
403
  templates = replace_refs(old["templates"] if old else [], args.template, "templates")
403
404
  rules = replace_refs(old["rules"] if old else [], args.rule, "rules")
404
405
  for family in args.remove_template:
405
406
  identifier(family)
406
407
  if not any(parse_ref(ref, "templates")[0].split("/")[0] == family for ref in templates):
407
- fail(f"无法移除未选择的模板系列:{family}")
408
+ fail(_("无法移除未选择的模板系列:{0}").format(family))
408
409
  templates = [ref for ref in templates if parse_ref(ref, "templates")[0].split("/")[0] != family]
409
410
  for identity in args.remove_rule:
410
411
  identifier(identity)
411
412
  if not any(parse_ref(ref, "rules")[0] == identity for ref in rules):
412
- fail(f"无法移除未直接选择的规则:{identity}")
413
+ fail(_("无法移除未直接选择的规则:{0}").format(identity))
413
414
  rules = [ref for ref in rules if parse_ref(ref, "rules")[0] != identity]
414
415
  catalog = load_catalog(args.catalog)
415
416
  if old:
@@ -417,11 +418,11 @@ def profile_command(args: argparse.Namespace) -> dict[str, Any]:
417
418
  for source in sources:
418
419
  existing = catalog[kind].get(source["ref"])
419
420
  if existing is not None and digest(existing) != source["sha256"]:
420
- fail(f"已经发布的来源内容发生变化:{source['ref']}")
421
+ fail(_("已经发布的来源内容发生变化:{0}").format(source['ref']))
421
422
  catalog[kind].setdefault(source["ref"], source["value"])
422
423
  custom = read_text(args.custom_file).strip() if args.custom_file else (old["custom"] if old else None)
423
424
  if args.loading_file and args.remove_loading:
424
- fail("--loading-file 与 --remove-loading 不能同时使用")
425
+ fail(_("--loading-file 与 --remove-loading 不能同时使用"))
425
426
  # 加载声明整体替换;未提供时沿用原 Profile,保证未变更的声明不会在修订中丢失。
426
427
  loading = read_object(args.loading_file) if args.loading_file else (None if args.remove_loading or not old else old.get("loading"))
427
428
  value = {
@@ -436,17 +437,17 @@ def profile_command(args: argparse.Namespace) -> dict[str, Any]:
436
437
  resolutions = read_object(args.resolutions_file) if args.resolutions_file else {}
437
438
  conflict_ids = {item["id"] for item in changes["conflicts"]}
438
439
  if set(resolutions) - conflict_ids:
439
- fail("resolutions 包含没有冲突的编号")
440
+ fail(_("resolutions 包含没有冲突的编号"))
440
441
  for identity, choice in resolutions.items():
441
442
  if choice == "use-upstream":
442
443
  value["overrides"].pop(identity, None)
443
444
  elif choice != "keep-local":
444
- fail("冲突处理必须为 keep-local 或 use-upstream")
445
+ fail(_("冲突处理必须为 keep-local 或 use-upstream"))
445
446
  overrides = read_object(args.overrides_file) if args.overrides_file else {}
446
447
  for identity, override in overrides.items():
447
448
  identifier(identity)
448
449
  if identity not in base_rules(value):
449
- fail(f"override 引用未知规则:{identity}")
450
+ fail(_("override 引用未知规则:{0}").format(identity))
450
451
  if override is None:
451
452
  value["overrides"][identity] = None
452
453
  else:
@@ -467,7 +468,7 @@ def profile_command(args: argparse.Namespace) -> dict[str, Any]:
467
468
  result = {"ok": True, "status": "preview", "path": str(path), "profile": value, "content": render(value), "effective_rules": list(effective_rules(value).values()), "changes": changes, "unresolved": sorted(unresolved), "confirmation_sha256": fingerprint}
468
469
  if args.apply:
469
470
  if unresolved:
470
- fail(f"升级存在未确认的冲突:{sorted(unresolved)}")
471
+ fail(_("升级存在未确认的冲突:{0}").format(sorted(unresolved)))
471
472
  confirm(args, fingerprint)
472
473
  result["status"] = immutable_write(path, value)
473
474
  return result
@@ -480,22 +481,22 @@ def publish_command(args: argparse.Namespace) -> dict[str, Any]:
480
481
  ref = release_ref(value, kind)
481
482
  existing = catalog[kind].get(ref)
482
483
  if existing is not None and existing != value:
483
- fail(f"已经发布的版本不可修改:{ref}")
484
+ fail(_("已经发布的版本不可修改:{0}").format(ref))
484
485
  identity, release = parse_ref(ref, kind)
485
486
  patch_peers = [item for key, item in catalog[kind].items() if parse_ref(key, kind)[0] == identity and version(item["version"])[:2] == version(release)[:2] and item["version"] != release]
486
487
  for latest in patch_peers:
487
488
  before, after = version(latest["version"]), version(release)
488
489
  if before[:2] == after[:2]:
489
490
  if kind == "rules" and behavior(latest) != behavior(value):
490
- fail("patch 版本只允许不改变行为的修改")
491
+ fail(_("patch 版本只允许不改变行为的修改"))
491
492
  if kind == "templates":
492
493
  if latest["scopes"] != value["scopes"]:
493
- fail("patch 版本不能改变适用范围")
494
+ fail(_("patch 版本不能改变适用范围"))
494
495
  a = resolve(catalog, [release_ref(latest, kind)], [])
495
496
  prospective = {**catalog, "templates": {**catalog["templates"], ref: value}}
496
497
  b = resolve(prospective, [ref], [])
497
498
  if {s["value"]["id"]: behavior(s["value"]) for s in a["rules"]} != {s["value"]["id"]: behavior(s["value"]) for s in b["rules"]}:
498
- fail("patch 版本不能改变有效规则")
499
+ fail(_("patch 版本不能改变有效规则"))
499
500
  catalog[kind][ref] = value
500
501
  if kind == "templates":
501
502
  sources = resolve(catalog, [ref], [])
@@ -507,7 +508,7 @@ def publish_command(args: argparse.Namespace) -> dict[str, Any]:
507
508
  if args.apply:
508
509
  confirm(args, fingerprint)
509
510
  if (root == PUBLIC_CATALOG or PUBLIC_CATALOG in root.parents) and not args.public_approved:
510
- fail("公开内容需要用户批准完整文件,并使用 --public-approved")
511
+ fail(_("公开内容需要用户批准完整文件,并使用 --public-approved"))
511
512
  result["status"] = immutable_write(path, value)
512
513
  return result
513
514
 
@@ -540,10 +541,10 @@ def lock_value(profile: dict[str, Any], content: str) -> dict[str, Any]:
540
541
  def validate_lock(lock: Any) -> dict[str, Any]:
541
542
  fields(lock, {"schema_version", "renderer_version", "profile", "profile_sha256", "opinion_sha256"}, "lock")
542
543
  if lock["schema_version"] != "2.0" or lock["renderer_version"] != "2.0":
543
- fail("锁定文件版本不受支持")
544
+ fail(_("锁定文件版本不受支持"))
544
545
  profile = validate_profile(lock["profile"])
545
546
  if lock != lock_value(profile, render(profile)):
546
- fail("锁定文件指纹与内容不一致")
547
+ fail(_("锁定文件指纹与内容不一致"))
547
548
  return lock
548
549
 
549
550
 
@@ -565,11 +566,11 @@ def atomic_write(path: Path, content: str) -> None:
565
566
 
566
567
  def compose_command(args: argparse.Namespace) -> dict[str, Any]:
567
568
  if args.template or args.rule or args.custom_file:
568
- fail("--profile 必须独立使用;通过 profile 命令修改选择和自定义内容")
569
+ fail(_("--profile 必须独立使用;通过 profile 命令修改选择和自定义内容"))
569
570
  profile = load_profile(args.profile)
570
571
  root = safe_path(args.project)
571
572
  if not root.is_dir():
572
- fail(f"项目目录不存在:{root}")
573
+ fail(_("项目目录不存在:{0}").format(root))
573
574
  content = render(profile)
574
575
  lock = lock_value(profile, content)
575
576
  destination, lock_path = root / "OPINION.md", root / "opinion.lock.json"
@@ -579,17 +580,17 @@ def compose_command(args: argparse.Namespace) -> dict[str, Any]:
579
580
  previous = validate_lock(json.loads(current_lock))
580
581
  previous_profile = previous["profile"]
581
582
  if (previous_profile["id"], previous_profile["version"]) == (profile["id"], profile["version"]) and digest(previous_profile) != digest(profile):
582
- fail("已经锁定的 Profile 版本不可改变内容;请发布新的 Profile 版本")
583
+ fail(_("已经锁定的 Profile 版本不可改变内容;请发布新的 Profile 版本"))
583
584
  if current is None:
584
585
  if previous != lock:
585
- blocked = "正文缺失;请先使用原锁定 Profile 重建正文,再进行升级"
586
+ blocked = _("正文缺失;请先使用原锁定 Profile 重建正文,再进行升级")
586
587
  elif text_digest(current) != previous["opinion_sha256"]:
587
588
  if args.accept_current != text_digest(current):
588
- blocked = "OPINION.md 已被人工修改;请读取并纳入新的 Profile,审阅后用 --accept-current 当前文件SHA256 确认"
589
+ blocked = _("OPINION.md 已被人工修改;请读取并纳入新的 Profile,审阅后用 --accept-current 当前文件SHA256 确认")
589
590
  elif current is not None and current != content and not args.replace:
590
591
  # 完整匹配初始化占位内容,任何追加内容均需要审阅。
591
592
  if current not in KNOWN_PLACEHOLDERS:
592
- blocked = "已有 OPINION.md 需要审阅并使用 --replace"
593
+ blocked = _("已有 OPINION.md 需要审阅并使用 --replace")
593
594
  fingerprint = digest({"lock": lock, "before": {"opinion": current, "lock": current_lock}, "replace": args.replace, "accept_current": args.accept_current})
594
595
  result = {"ok": True, "status": "preview", "content": content, "lock": lock, "blocked": blocked, "confirmation_sha256": fingerprint, "path": str(destination)}
595
596
  if args.apply:
@@ -611,7 +612,7 @@ def verify_project(project: Path) -> dict[str, Any]:
611
612
  root = safe_path(project)
612
613
  lock = validate_lock(read_object(root / "opinion.lock.json"))
613
614
  if text_digest(read_text(root / "OPINION.md")) != lock["opinion_sha256"]:
614
- fail("OPINION.md 与锁定文件不一致;请读取人工修改或检查中断的写入")
615
+ fail(_("OPINION.md 与锁定文件不一致;请读取人工修改或检查中断的写入"))
615
616
  return {"ok": True, "status": "verified", "profile": f"{lock['profile']['id']}@{lock['profile']['version']}", "profile_sha256": lock["profile_sha256"], "opinion_sha256": lock["opinion_sha256"]}
616
617
 
617
618
 
@@ -622,7 +623,7 @@ def updates_command(args: argparse.Namespace) -> dict[str, Any]:
622
623
  for source in sources:
623
624
  item = catalog[kind].get(source["ref"])
624
625
  if item is not None and digest(item) != source["sha256"]:
625
- fail(f"已经发布的来源内容发生变化:{source['ref']}")
626
+ fail(_("已经发布的来源内容发生变化:{0}").format(source['ref']))
626
627
  updates = []
627
628
  for kind in ("templates", "rules"):
628
629
  for ref in profile[kind]:
@@ -639,15 +640,15 @@ def options(parser: argparse.ArgumentParser, *, catalog: bool = False, mutation:
639
640
  parser.add_argument("--catalog", type=Path, default=PUBLIC_CATALOG)
640
641
  if mutation:
641
642
  parser.add_argument("--apply", action="store_true")
642
- parser.add_argument("--confirm", help="预览返回的完整内容 SHA-256")
643
+ parser.add_argument("--confirm", help=_("预览返回的完整内容 SHA-256"))
643
644
 
644
645
 
645
646
  def add_commands(subparsers: Any, compose: argparse.ArgumentParser, catalog: argparse.ArgumentParser) -> None:
646
647
  compose.add_argument("--profile", type=Path)
647
648
  compose.add_argument("--confirm")
648
- compose.add_argument("--accept-current", help="人工修改或写入中断后的当前 OPINION.md SHA-256;读取并保留内容后使用")
649
+ compose.add_argument("--accept-current", help=_("人工修改或写入中断后的当前 OPINION.md SHA-256;读取并保留内容后使用"))
649
650
  catalog.add_argument("--catalog", type=Path)
650
- profile = subparsers.add_parser("profile", help="预览、派生和发布不可变的个人 Profile")
651
+ profile = subparsers.add_parser("profile", help=_("预览、派生和发布不可变的个人 Profile"))
651
652
  options(profile, catalog=True, mutation=True)
652
653
  profile.add_argument("--project", required=True, type=Path)
653
654
  profile.add_argument("--id", required=True)
@@ -660,20 +661,20 @@ def add_commands(subparsers: Any, compose: argparse.ArgumentParser, catalog: arg
660
661
  profile.add_argument("--from-profile", type=Path)
661
662
  profile.add_argument("--overrides-file", type=Path)
662
663
  profile.add_argument("--resolutions-file", type=Path)
663
- profile.add_argument("--loading-file", type=Path, help="完整的分层加载声明 JSON;整体替换原声明")
664
- profile.add_argument("--remove-loading", action="store_true", help="移除分层加载声明,恢复为完整读取")
665
- publish = subparsers.add_parser("publish", help="预览或发布规则、模板版本")
664
+ profile.add_argument("--loading-file", type=Path, help=_("完整的分层加载声明 JSON;整体替换原声明"))
665
+ profile.add_argument("--remove-loading", action="store_true", help=_("移除分层加载声明,恢复为完整读取"))
666
+ publish = subparsers.add_parser("publish", help=_("预览或发布规则、模板版本"))
666
667
  options(publish, catalog=True, mutation=True)
667
668
  publish.add_argument("--file", required=True, type=Path)
668
669
  publish.add_argument("--public-approved", action="store_true")
669
- updates = subparsers.add_parser("updates", help="只读检查同一变体的新版本")
670
+ updates = subparsers.add_parser("updates", help=_("只读检查同一变体的新版本"))
670
671
  options(updates, catalog=True)
671
672
  updates.add_argument("--profile", required=True, type=Path)
672
- compare = subparsers.add_parser("compare", help="查看两份 Profile 的差异和三方冲突")
673
+ compare = subparsers.add_parser("compare", help=_("查看两份 Profile 的差异和三方冲突"))
673
674
  options(compare)
674
675
  compare.add_argument("--from-profile", required=True, type=Path)
675
676
  compare.add_argument("--to-profile", required=True, type=Path)
676
- verify = subparsers.add_parser("verify", help="核对正文、Profile 快照和锁定指纹")
677
+ verify = subparsers.add_parser("verify", help=_("核对正文、Profile 快照和锁定指纹"))
677
678
  options(verify)
678
679
  verify.add_argument("--project", required=True, type=Path)
679
680
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: opinion-manager
3
- description: Generate, compose, review and maintain behavior rules between the user and the Agent. Use when the user asks to create or update OPINION.md, import existing instructions, apply a template directly, pick some of its items, start from blank, propose rule candidates from authorized memory, maintain a personal Profile, check upgrades and conflicts, or review a deliverable against the current Opinion. 中文:生成、组合、审查和维护用户与 Agent 之间的行为规则。用户要求创建或更新 OPINION.md、导入既有说明、直接用模板、选择部分条目、从空白开始、基于已授权记忆提出候选规则、维护个人 Profile、检查升级与冲突或按当前 Opinion 审查交付物时使用。
3
+ description: Generate, compose, review and maintain behavior rules between the user and the Agent. Use when the user asks to create or update OPINION.md, import existing instructions, apply a template directly, pick some of its items, start from blank, propose rule candidates from authorized memory, maintain a personal Profile, check upgrades and conflicts, or review a deliverable against the current Opinion. Also use during ordinary work when the user states a lasting preference or a correction of your behavior (stash it as a candidate), at task completion to confirm new candidates, and when unconfirmed candidates have piled up. 中文:生成、组合、审查和维护用户与 Agent 之间的行为规则。用户要求创建或更新 OPINION.md、导入既有说明、直接用模板、选择部分条目、从空白开始、基于已授权记忆提出候选规则、维护个人 Profile、检查升级与冲突或按当前 Opinion 审查交付物时使用。工作中用户表达长期偏好或对你行为的纠正时暂存为候选,任务完成时集中请用户确认,候选堆积时协助逐条裁决。
4
4
  ---
5
5
 
6
6
  # Opinion Manager(Agent 行为规则管理)
@@ -49,10 +49,23 @@ description: Generate, compose, review and maintain behavior rules between the u
49
49
 
50
50
  ## 交付物审查与旧格式
51
51
 
52
- 实施前完整读取当前项目 `OPINION.md`,按条件选择适用条款;交付前逐条检查并修正明确问题。Profile 声明了分层加载时,改为先运行 `context core` 读取核心规则与规则束索引,任务命中触发条件或判断不清时用 `context bundle` 读取对应规则束,交付前用 `context check` 核对;命令返回 `fallback` 时完整读取正文。维护规则本身时始终完整读取。冲突改变交付时集中请用户裁决。用户反馈只形成候选,经确认才能发布新版本。项目需要记录候选发生次数、灰度使用与规则覆盖时,使用 `lifecycle` 命令在项目私有目录记账:任务结束后提交 `use` 事件,如实记录适用规则、逐条结果、读取的规则束和漏读;测试与演示标为 `fixture`。工具只记账,不在后台学习,也不自动晋升候选或把灰度版本转为正式,这些裁决来自用户。
52
+ 实施前完整读取当前项目 `OPINION.md`,按条件选择适用条款;交付前逐条检查并修正明确问题。Profile 声明了分层加载时,改为先运行 `context core` 读取核心规则与规则束索引,任务命中触发条件或判断不清时用 `context bundle` 读取对应规则束,交付前用 `context check` 核对;命令返回 `fallback` 时完整读取正文。维护规则本身时始终完整读取。冲突改变交付时集中请用户裁决。用户反馈只形成候选,经确认才能发布新版本。项目需要记录候选发生次数、灰度使用与规则覆盖时,使用 `lifecycle` 命令在项目私有目录记账:任务结束后提交 `use` 事件,如实记录适用规则、逐条结果、读取的规则束和漏读;测试与演示标为 `fixture`。工具只记账,不在后台学习,也不自动晋升候选或把灰度版本转为正式,这些裁决来自用户。工作中的低成本暂存使用 `inbox`(见下文),`lifecycle` 保留给带证据的灰度与覆盖记账。
53
53
 
54
54
  旧格式 `templates/*.json`、`compose --template`、`compose --rule` 与 `compose --custom-file` 保持兼容。有锁项目必须使用版本化 Profile 流程,禁止用旧格式写入绕过核验;新项目优先使用版本化 catalog。
55
55
 
56
+ ## 候选收件箱与自动采集
57
+
58
+ 收件箱让用户在工作中顺手沉淀观点:先低成本暂存,再由用户集中裁决。agent-pack 的 hook(`agenthelm nudge`)会在会话开始注入下面的协议并提示未决事项;没有 hook 的客户端按本节手动执行。命令与数据格式见 [候选收件箱](../../references/inbox.md),hook 与阈值见 agent-pack 的 `references/nudge.md`。
59
+
60
+ - **暂存**:用户在工作中表达长期偏好、对你行为的纠正或规则(“以后都……”“不要再……”“记住……”)时,立即用 `inbox add` 暂存标题、完整规则、适用范围和用户原话。不打断当前任务,不需要确认。一次性的任务要求、项目事实和命令不是候选。宁可多暂存,裁决环节会过滤。
61
+ - **匹配**:实施前读取 OPINION.md;用户已同意试行的候选(`inbox list --status trial`,hook 会随协议注入)同样遵循,交付时说明是否适用。
62
+ - **确认**:任务完成或即将完成时,把本次会话新增的候选用编号列表一次性交给用户:正式采纳 / 项目内试行 / 暂缓 / 丢弃。用户明确表态后用 `inbox decide … --confirmed` 记录;每个会话集中确认一次,不逐条追问。
63
+ - **转正式**:`formal` 只记录用户意愿。写入 OPINION.md 仍走上文的完整预览与确认流程,候选正文作为个人规则输入。
64
+ - **边界**:`inbox` 不写 OPINION.md、Profile 或锁;重复出现不等于确认。试行超过设定天数未转正会回到待确认。
65
+ - **隐私**:收件箱 `.opinion/inbox.json` 保存用户原话,可能含敏感信息,建议项目保持私有。`inbox add` 返回 `notice` 时如实转告用户;是否公开仓库、是否排除 `.opinion/` 由用户决定,不要替用户决定,也不要自行修改 `.gitignore`。
66
+ - **语言**:提示与协议有中文、英文两种,命令带 `--lang zh|en`;向用户转告和确认时使用用户正在使用的语言。候选内容是用户自己的话,不翻译。
67
+ - **未决汇总**:开新会话时 agent-pack 按阈值提示待确认数量与久未处理的天数;用户可在 Project Profile 的 `opinion.nudge` 或 `~/.config/agenthelm/config.json` 的 `nudge` 调整,或用 `AGENTHELM_NO_NUDGE=1` 关闭。
68
+
56
69
  ## 完成条件
57
70
 
58
71
  - 选择从空白开始时,已有规则保持不变,不创建空规则版本;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-orchestrator",
3
- "version": "1.8.1",
3
+ "version": "1.11.1",
4
4
  "description": "Portable project lifecycle orchestration for configured workspaces.",
5
5
  "author": {
6
6
  "name": "lc4t"
@@ -3,10 +3,10 @@
3
3
  "packs": [
4
4
  {
5
5
  "id": "project-orchestrator/case-template",
6
- "version": "1.0.0",
6
+ "version": "1.1.0",
7
7
  "kind": "sample",
8
8
  "title": "默认 Case 模板与 Schema",
9
- "description": "runtime/case.py 新建 Case 时使用的 CASE.md 正文模板与默认 Frontmatter Schema;只影响之后新建的 Case,已有 Case 不会被改写。",
9
+ "description": "runtime/case.py 新建 CASE.md 时使用的正文模板(中文 CASE.md、英文 CASE.en.md,按 Project Profile 的 language 选择)与默认 Frontmatter Schema;只影响之后新建的 Case,已有 Case 不会被改写。(1.1.0:Schema 增加可选字段 sop,记录这个 Case 是哪个 SOP 的一次执行。1.2.0:新增 CASE.en.md。)",
10
10
  "sources": [
11
11
  "templates",
12
12
  "references/case.schema.json"
@@ -14,7 +14,8 @@
14
14
  "materializes": [],
15
15
  "project_lock": "none",
16
16
  "files": {
17
- "references/case.schema.json": "7e56e407926a3e49434102ca87bcd5e48174357f527afd8952ab9af993732205",
17
+ "references/case.schema.json": "ecb37f084b901786bdc878cdc7f1e5d74c2eb31722d6a10fe24f6d88768bcbde",
18
+ "templates/CASE.en.md": "4aec0fab19c83bb5c2b72a31bbedb3425c82efb13e5d436f5cee35496d6eab74",
18
19
  "templates/CASE.md": "a33ffcec5c239cbcf4c0828f08336c578d6d184363b01db1419f74fcdc9e1086"
19
20
  }
20
21
  },
@@ -32,6 +33,32 @@
32
33
  "files": {
33
34
  "references/asset-synonyms.example.json": "a3338b98addd2b0fa386829850e0dd2667dff593ac7aa7f8e70b91372b3439c9"
34
35
  }
36
+ },
37
+ {
38
+ "id": "project-orchestrator/sop-samples",
39
+ "version": "1.0.0",
40
+ "kind": "sample",
41
+ "title": "SOP 样例",
42
+ "description": "runtime/sop.py new --template 使用的 SOP 样例:blank(空白,带填写说明)、video-to-transcript(视频转文字稿,展示工具清单、条件性前置、替代方案与中间产物)、weekly-review(周期性复盘,展示先确定复盘范围)、knowledge-digest(知识整理,展示可共享的流程与规则候选的交接)。每个样例有中文(SOP.md)和英文(SOP.en.md)两套,按项目文件语言选用;样例只含流程结构,不含任何个人进度或数据;创建的都是草稿,需要与用户谈清各项约定后才能启用;已创建的 SOP 不会被改写。",
43
+ "sources": [
44
+ "sop-templates"
45
+ ],
46
+ "materializes": [],
47
+ "project_lock": "none",
48
+ "files": {
49
+ "sop-templates/blank/SOP.en.md": "f7929e5840f1a4ea7cb832d949237be43328c662c690fcc2e204bb36233940fa",
50
+ "sop-templates/blank/SOP.md": "efec23ad29102590bdd16268c79222eb6f14fa8a1352deb27a5e55d35c413704",
51
+ "sop-templates/knowledge-digest/SOP.en.md": "e30bb6fb0282f23e326e70404ac66e4419c99a53a2e3eb2cabfdffa5c2754e55",
52
+ "sop-templates/knowledge-digest/SOP.md": "eaa8d7eb2338fe71b7a05fec662230a7e0b2e36f4727657558f2b51ed0a98027",
53
+ "sop-templates/knowledge-digest/templates/digest.en.md": "9461e971a40307434d5460b12cf0938038ab6af2b2c98ce5a02260d816bc9d7a",
54
+ "sop-templates/knowledge-digest/templates/digest.md": "0741e5a8008119528c5fc71cc9c3c8064dc1173d61465643e8dc5745883ccb9a",
55
+ "sop-templates/video-to-transcript/SOP.en.md": "5c5797eed4fdadaec4089b43843bc43287d68cc6699e9fafd1803aa8e8e253a4",
56
+ "sop-templates/video-to-transcript/SOP.md": "b8dfb1ecb0e2cf2a13d4acad78ee759f6c402776443cf5a70e2d6390a120ba4e",
57
+ "sop-templates/weekly-review/SOP.en.md": "fd9734078467066463c8005c459a9c3885236545e622b1219c52cf5633045de8",
58
+ "sop-templates/weekly-review/SOP.md": "9dee7bcbf351e395a66e042929eede0bb425a92a88db14944b4348f2de656b1b",
59
+ "sop-templates/weekly-review/templates/review.en.md": "ba727876879208caad46c6ce9c51411e1f451e72003572ccd18de9ef6f55811e",
60
+ "sop-templates/weekly-review/templates/review.md": "cf8321a1739cecf6286d591e88a6af86831b01c12e77834a873ea497a5db127c"
61
+ }
35
62
  }
36
63
  ]
37
64
  }