@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.
- package/package.json +1 -1
- package/runtime/index.json +6 -6
- package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/agent-pack/hooks/hooks.json +32 -0
- package/runtime/plugins/agent-pack/plugin.json +1 -1
- package/runtime/plugins/agent-pack/references/local-only.md +126 -0
- package/runtime/plugins/agent-pack/references/nudge.en.md +58 -0
- package/runtime/plugins/agent-pack/references/nudge.md +58 -0
- package/runtime/plugins/agent-pack/runtime/agent_pack_nudge.py +573 -0
- package/runtime/plugins/agent-pack/runtime/agenthelm.py +419 -107
- package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +77 -53
- package/runtime/plugins/agent-pack/runtime/i18n_agent_pack.py +78 -0
- package/runtime/plugins/agent-pack/runtime/local_only.py +1130 -0
- package/runtime/plugins/agent-pack/runtime/locale/en.json +385 -0
- package/runtime/plugins/agent-pack/runtime/mcp_server.py +25 -0
- package/runtime/plugins/agent-pack/runtime/nudge_text.py +107 -0
- package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +9 -1
- package/runtime/plugins/agents-init/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/agents-init/content.json +17 -2
- package/runtime/plugins/agents-init/plugin.json +1 -1
- package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +47 -0
- package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +39 -1
- package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +404 -70
- package/runtime/plugins/agents-init/skills/agents-init/scripts/adopt.py +596 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/i18n_init.py +78 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +245 -72
- package/runtime/plugins/agents-init/skills/agents-init/scripts/locale/en.json +210 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/regions.py +99 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +1081 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_adopt.py +551 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +46 -2
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +555 -0
- package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +66 -0
- package/runtime/plugins/agents-init/skills/agents-init/skill.json +1 -1
- package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/references/inbox.en.md +48 -0
- package/runtime/plugins/opinion-manager/references/inbox.md +48 -0
- package/runtime/plugins/opinion-manager/runtime/context.py +19 -18
- package/runtime/plugins/opinion-manager/runtime/i18n_opinion.py +78 -0
- package/runtime/plugins/opinion-manager/runtime/inbox.py +397 -0
- package/runtime/plugins/opinion-manager/runtime/inbox_text.py +101 -0
- package/runtime/plugins/opinion-manager/runtime/lifecycle.py +45 -44
- package/runtime/plugins/opinion-manager/runtime/locale/en.json +184 -0
- package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +55 -50
- package/runtime/plugins/opinion-manager/runtime/versioned.py +84 -83
- package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +15 -2
- package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/content.json +30 -3
- package/runtime/plugins/project-orchestrator/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/references/case-workspaces.md +9 -1
- package/runtime/plugins/project-orchestrator/references/case.schema.json +1 -0
- package/runtime/plugins/project-orchestrator/references/sop-interview.md +83 -0
- package/runtime/plugins/project-orchestrator/references/sop.md +156 -0
- package/runtime/plugins/project-orchestrator/runtime/assets.py +52 -57
- package/runtime/plugins/project-orchestrator/runtime/case.py +139 -80
- package/runtime/plugins/project-orchestrator/runtime/case_structure.py +71 -0
- package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +47 -46
- package/runtime/plugins/project-orchestrator/runtime/i18n_orchestrator.py +78 -0
- package/runtime/plugins/project-orchestrator/runtime/locale/en.json +1409 -0
- package/runtime/plugins/project-orchestrator/runtime/memory.py +44 -43
- package/runtime/plugins/project-orchestrator/runtime/private_paths.py +119 -0
- package/runtime/plugins/project-orchestrator/runtime/project_base.py +37 -19
- package/runtime/plugins/project-orchestrator/runtime/project_cli.py +267 -174
- package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
- package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +524 -134
- package/runtime/plugins/project-orchestrator/runtime/project_menu.py +159 -85
- package/runtime/plugins/project-orchestrator/runtime/project_real.py +191 -106
- package/runtime/plugins/project-orchestrator/runtime/project_write.py +58 -55
- package/runtime/plugins/project-orchestrator/runtime/sop.py +1670 -0
- package/runtime/plugins/project-orchestrator/runtime/sop_structure.py +226 -0
- package/runtime/plugins/project-orchestrator/runtime/wf.py +74 -70
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +22 -0
- package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.en.md +57 -0
- package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.md +57 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.en.md +77 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.md +77 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.en.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.en.md +87 -0
- package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.md +87 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.en.md +75 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.md +75 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.en.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.md +12 -0
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
185
|
+
identity, _version = parse_ref(ref, "templates")
|
|
185
186
|
family = identity.split("/")[0]
|
|
186
187
|
if family in families:
|
|
187
|
-
fail(
|
|
188
|
+
fail(_("同一模板系列只能选择一个变体和版本:{0}").format(family))
|
|
188
189
|
families.add(family)
|
|
189
190
|
if ref not in catalog["templates"]:
|
|
190
|
-
fail(
|
|
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,
|
|
197
|
+
identity, _version = parse_ref(ref, "rules")
|
|
197
198
|
if ref not in catalog["rules"]:
|
|
198
|
-
fail(
|
|
199
|
+
fail(_("未知规则:{0}").format(ref))
|
|
199
200
|
value = catalog["rules"][ref]
|
|
200
201
|
if identity in selected and selected[identity]["ref"] != ref:
|
|
201
|
-
fail(
|
|
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(
|
|
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(
|
|
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(
|
|
254
|
+
fail(_("loading.bundles 引用了未选择的模板系列:{0!r}").format(family))
|
|
254
255
|
if family in seen:
|
|
255
|
-
fail(
|
|
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(
|
|
261
|
+
fail(_("规则束至少需要一条路由:{0}").format(family))
|
|
261
262
|
for route in bundle["routes"]:
|
|
262
263
|
if not isinstance(route, dict) or not route:
|
|
263
|
-
fail(
|
|
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(
|
|
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(
|
|
290
|
+
fail(_("source 指纹或引用不一致:{0}").format(source['ref']))
|
|
290
291
|
if source["ref"] in catalog[kind]:
|
|
291
|
-
fail(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
- 选择从空白开始时,已有规则保持不变,不创建空规则版本;
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"packs": [
|
|
4
4
|
{
|
|
5
5
|
"id": "project-orchestrator/case-template",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.1.0",
|
|
7
7
|
"kind": "sample",
|
|
8
8
|
"title": "默认 Case 模板与 Schema",
|
|
9
|
-
"description": "runtime/case.py 新建
|
|
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": "
|
|
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
|
}
|