codebee 0.1.66 → 0.1.68

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 (116) hide show
  1. package/CHANGELOG.md +543 -520
  2. package/LICENSE +21 -21
  3. package/README.md +11 -7
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -0
  12. package/app/core/aiflavor.py +197 -197
  13. package/app/core/artifacts.py +148 -148
  14. package/app/core/assets.py +110 -0
  15. package/app/core/attachments.py +623 -532
  16. package/app/core/automation.py +758 -758
  17. package/app/core/backup.py +470 -470
  18. package/app/core/beekeeper.py +183 -183
  19. package/app/core/board.py +209 -209
  20. package/app/core/bookmeta.py +930 -796
  21. package/app/core/bookmeta_catalog.py +180 -180
  22. package/app/core/branching.py +137 -137
  23. package/app/core/builtin_agent.py +1234 -1234
  24. package/app/core/capability.py +144 -144
  25. package/app/core/catalog.py +42 -0
  26. package/app/core/chaptersafe.py +32 -32
  27. package/app/core/cleanup.py +303 -303
  28. package/app/core/compaction.py +201 -201
  29. package/app/core/covergen.py +356 -356
  30. package/app/core/defectretro.py +43 -0
  31. package/app/core/diagnostics.py +114 -114
  32. package/app/core/dispatch.py +173 -173
  33. package/app/core/dispatch_log.py +128 -113
  34. package/app/core/env_scrub.py +85 -85
  35. package/app/core/error_codes.py +23 -23
  36. package/app/core/errorlog.py +137 -137
  37. package/app/core/evaluation.py +174 -174
  38. package/app/core/failure_notify.py +290 -290
  39. package/app/core/flows.py +48 -2
  40. package/app/core/goal_service.py +167 -167
  41. package/app/core/health.py +374 -374
  42. package/app/core/history.py +38 -38
  43. package/app/core/hooks.py +162 -162
  44. package/app/core/jobs.py +46 -26
  45. package/app/core/knowledge.py +57 -3
  46. package/app/core/manager.py +2122 -2115
  47. package/app/core/market.py +362 -362
  48. package/app/core/market_remote.py +124 -26
  49. package/app/core/mocks.py +64 -64
  50. package/app/core/modelhub.py +51 -3
  51. package/app/core/notify.py +106 -106
  52. package/app/core/operations.py +228 -0
  53. package/app/core/paihang.py +138 -109
  54. package/app/core/paths.py +2 -2
  55. package/app/core/pipeline.py +221 -12
  56. package/app/core/planner.py +6 -1
  57. package/app/core/portguard.py +111 -111
  58. package/app/core/preview.py +293 -293
  59. package/app/core/publish/__init__.py +7 -7
  60. package/app/core/publish/auto.py +367 -367
  61. package/app/core/publish/browser.py +539 -533
  62. package/app/core/publish/fanqie.py +159 -128
  63. package/app/core/publish/flow.py +440 -432
  64. package/app/core/publish/flows-fanqie-calibrated.json +332 -332
  65. package/app/core/publish/flows-qimao-calibrated.json +7 -3
  66. package/app/core/publish/ledger.py +203 -198
  67. package/app/core/publish/manager.py +692 -623
  68. package/app/core/publish/qimao.py +141 -141
  69. package/app/core/publish/ws.py +139 -139
  70. package/app/core/redact.py +38 -38
  71. package/app/core/repeat_guard.py +129 -129
  72. package/app/core/revisions.py +51 -0
  73. package/app/core/router.py +398 -398
  74. package/app/core/runner.py +2067 -2050
  75. package/app/core/selfupdate.py +384 -384
  76. package/app/core/session_log.py +161 -161
  77. package/app/core/sessions.py +312 -312
  78. package/app/core/settings.py +149 -149
  79. package/app/core/settings_schema.py +262 -262
  80. package/app/core/share_page.py +114 -114
  81. package/app/core/skill_scan.py +122 -114
  82. package/app/core/skillpacks/fanqie-novel.md +80 -80
  83. package/app/core/skillpacks/market/character-bible.md +66 -66
  84. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  85. package/app/core/skillpacks/market/git-workflow.md +57 -57
  86. package/app/core/skillpacks/market/release-notes.md +72 -72
  87. package/app/core/skillpacks/market/weekly-report.md +71 -71
  88. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  89. package/app/core/skills.py +109 -2
  90. package/app/core/step_runner.py +132 -132
  91. package/app/core/store.py +278 -41
  92. package/app/core/task_compile.py +199 -199
  93. package/app/core/telemetry.py +292 -292
  94. package/app/core/tlsctx.py +64 -64
  95. package/app/core/token_meter.py +147 -147
  96. package/app/core/tracing.py +144 -0
  97. package/app/core/upstream.py +41 -41
  98. package/app/core/usage.py +862 -858
  99. package/app/core/volumes.py +389 -355
  100. package/app/core/wxdigest.py +858 -858
  101. package/app/core/zentao.py +2465 -2447
  102. package/app/main.py +2981 -2838
  103. package/app/pick_dialog.py +110 -110
  104. package/app/ui/app.js +14399 -13994
  105. package/app/ui/artdiff.js +74 -0
  106. package/app/ui/board.html +162 -162
  107. package/app/ui/board.js +305 -305
  108. package/app/ui/i18n.js +2770 -2716
  109. package/app/ui/icons/bee.svg +79 -79
  110. package/app/ui/index.html +6 -0
  111. package/app/ui/manifest.json +16 -16
  112. package/app/ui/qrcode.js +2297 -2297
  113. package/app/ui/style.css +96 -24
  114. package/bin/tutti.js +147 -147
  115. package/package.json +46 -46
  116. package/tools/wx_reader.py +261 -261
package/app/core/flows.py CHANGED
@@ -9,6 +9,7 @@
9
9
  """
10
10
  from __future__ import annotations
11
11
 
12
+ import hashlib
12
13
  import json
13
14
  import re
14
15
  import threading
@@ -76,7 +77,10 @@ BUILTIN_FLOWS = [
76
77
  "note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
77
78
  {"id": "rank_scan", "name": "扫榜选材", "icon": "i-chart", "engine": "direct", "builtin": True,
78
79
  "goal_hint": "想写哪个方向(一句话,可留空默认分析总榜热门题材)",
79
- "note": "抓取七猫+番茄排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点(快档直出报告)"},
80
+ "note": "抓取七猫/番茄/起点/纵横四平台排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点(快档直出报告)"},
81
+ {"id": "defect_retro", "name": "缺陷复盘", "icon": "i-history", "engine": "direct", "builtin": True,
82
+ "goal_hint": "复盘哪个版本/时间段的缺陷(建议附带禅道或 Jira 导出 CSV)",
83
+ "note": "读取附带的缺陷导出数据 → AI 输出产品/开发/测试三视角复盘报告:整体画像、根因聚类、漏测分析、改进动作(快档直出报告)"},
80
84
  {"id": "research", "name": "调研报告", "icon": "i-file-search", "engine": "review", "builtin": True,
81
85
  "manuscript": "report.md",
82
86
  "rubric": ["全面性", "深度", "论据可靠", "可读性", "结论质量"],
@@ -113,7 +117,7 @@ BUILTIN_FLOWS = [
113
117
  "threshold": 7.0, "rounds": 2,
114
118
  "goal_hint": "目标岗位 + 个人经历(可贴旧简历附件),几年经验投什么职级",
115
119
  "note": "简历优化/定制:起草 → 多维评审 → 修订循环 → 发布门禁"},
116
- {"id": "bid_doc", "name": "标书编制", "icon": "i-file-search", "engine": "review", "builtin": True,
120
+ {"id": "bid_doc", "name": "标书编制", "icon": "i-tasks", "engine": "review", "builtin": True,
117
121
  "manuscript": "bid.md",
118
122
  "rubric": ["应答完整性", "合规符合度", "方案针对性", "评分点覆盖", "商务清晰度"],
119
123
  "threshold": 7.5, "rounds": 2,
@@ -250,6 +254,48 @@ def get_flow(flow_id):
250
254
  return None
251
255
 
252
256
 
257
+ # 参与 digest 的语义字段:会进运行链的参数与提示词。icon/note/goal_hint/name 是
258
+ # 纯展示位——措辞变化不影响执行语义,不应制造「修订漂移」噪音。
259
+ _DIGEST_FIELDS = ("id", "engine", "manuscript", "rubric", "threshold", "rounds",
260
+ "best_of", "serial", "draft_prompt", "critique_prompt",
261
+ "verify_command")
262
+
263
+
264
+ def flow_content_sha256(flow):
265
+ """Return the full semantic content hash for an immutable flow revision."""
266
+ if not isinstance(flow, dict):
267
+ return ""
268
+ basis = {k: flow.get(k) for k in _DIGEST_FIELDS}
269
+ blob = json.dumps(basis, ensure_ascii=False, sort_keys=True, default=str)
270
+ return hashlib.sha256(blob.encode("utf-8")).hexdigest()
271
+
272
+
273
+ def flow_digest(flow):
274
+ """流程定义的内容指纹(sha256 前 16 位)——「这个任务当时用的是哪一版流程」
275
+ 的对账真源(借鉴 WorkDSH ADR-0010 发布后不可变修订)。"""
276
+ return flow_content_sha256(flow)[:16]
277
+
278
+
279
+ def flow_drift(task):
280
+ """任务流程修订漂移检测:任务创建时钉的 digest vs 当前流程定义。
281
+
282
+ 任务参数在创建时已固化(改流程不影响固化字段),但 task_compile 对历史
283
+ 任务缺失字段(threshold/rubric/manuscript)仍回落当前流程定义——续跑/
284
+ 重试因此可能静默吃到新流程。这里只诊断不裁定:返回
285
+ {"flow", "pinned", "current"} 或 None(无快照/流程已删/无漂移)。"""
286
+ snap = (task or {}).get("flow_snapshot") or {}
287
+ pinned = str(snap.get("digest") or "")
288
+ if not pinned:
289
+ return None
290
+ cur = get_flow(str((task or {}).get("type") or ""))
291
+ if not cur:
292
+ return None
293
+ current = flow_digest(cur)
294
+ if not current or current == pinned:
295
+ return None
296
+ return {"flow": cur.get("id") or "", "pinned": pinned, "current": current}
297
+
298
+
253
299
  def upsert_flow(payload):
254
300
  """新增/更新流程。返回 (flow, 错误)。
255
301
 
@@ -1,168 +1,168 @@
1
- # -*- coding: utf-8 -*-
2
- """Goal 状态外置:跨轮目标(title/描述/phase/轮次)独立于 LLM 调用持久化。
3
-
4
- 设计稿:docs/migration/03-state-externalization.md §2A。
5
- 参考 dsh packages/goal/goal/:状态 100% 来自事件溯源(goal/change 事件),
6
- 唯一当前 goal,revision 走 CAS 拒绝陈旧引用。
7
-
8
- 与 planner.py 的关系:planner 继续负责「LLM 拆解」;GoalService 只管
9
- 「目标状态」——谁都可以读(UI/续行驱动器/流水线),变更必须过 CAS。
10
- """
11
- from __future__ import annotations
12
-
13
- import json
14
- import threading
15
- import time
16
- import uuid
17
- from pathlib import Path
18
-
19
- _LOCK = threading.RLock()
20
- # goal_id -> dict(内存缓存;磁盘为唯一事实来源)
21
- _GOALS = {}
22
- _FILE = None # 由 init() 注入
23
-
24
-
25
- def init(data_dir=None):
26
- """注入存储路径并从磁盘加载。测试可传临时目录。"""
27
- global _FILE, _GOALS
28
- with _LOCK:
29
- base = Path(data_dir) if data_dir else Path(_default_dir())
30
- base.mkdir(parents=True, exist_ok=True)
31
- _FILE = base / "goals.json"
32
- _GOALS = {}
33
- try:
34
- data = json.loads(_FILE.read_text(encoding="utf-8"))
35
- for g in data.get("goals") or []:
36
- if isinstance(g, dict) and g.get("goal_id"):
37
- _GOALS[g["goal_id"]] = g
38
- except (OSError, json.JSONDecodeError):
39
- pass
40
-
41
-
42
- def _default_dir():
43
- from . import paths
44
- return str(paths.DATA_DIR)
45
-
46
-
47
- def _persist():
48
- """原子写盘(tmp + replace)。调用方必须已持有 _LOCK。"""
49
- if _FILE is None:
50
- return
51
- tmp = _FILE.with_suffix(".tmp")
52
- tmp.write_text(json.dumps({"goals": list(_GOALS.values())},
53
- ensure_ascii=False, indent=2), encoding="utf-8")
54
- tmp.replace(_FILE)
55
-
56
-
57
- class StaleRevisionError(Exception):
58
- """CAS 失败:调用方持有的是陈旧 revision。"""
59
-
60
-
61
- class GoalExistsError(Exception):
62
- """已有未完结的当前 goal,需先 complete/abandon。"""
63
-
64
-
65
- class GoalNotFoundError(Exception):
66
- pass
67
-
68
-
69
- def _valid_phase(p):
70
- return p in ("active", "paused", "complete", "abandoned")
71
-
72
-
73
- def current():
74
- """返回唯一「当前」goal(未 complete/abandoned 的最新一个);无则 None。"""
75
- with _LOCK:
76
- open_goals = [g for g in _GOALS.values()
77
- if g.get("phase") in ("active", "paused")]
78
- if not open_goals:
79
- return None
80
- return max(open_goals, key=lambda g: g.get("created_at", 0))
81
-
82
-
83
- def get(goal_id: str):
84
- with _LOCK:
85
- g = _GOALS.get(goal_id)
86
- return dict(g) if g else None
87
-
88
-
89
- def create(title: str, description: str = "", *, rounds_max: int | None = None,
90
- metadata: dict | None = None) -> dict:
91
- """创建新 goal。已有未完结 goal 时拒绝(单一当前目标,仿 dsh)。
92
-
93
- rounds_max 缺省读 settings_v2 orchestrator.max_goal_rounds(读不到回落 5)。"""
94
- if rounds_max is None:
95
- try:
96
- from .settings_schema import get as ss_get, register_default_namespaces
97
- register_default_namespaces()
98
- rounds_max = int(ss_get("orchestrator", "max_goal_rounds") or 5)
99
- except Exception:
100
- rounds_max = 5
101
- with _LOCK:
102
- cur = current()
103
- if cur is not None:
104
- raise GoalExistsError(
105
- "已有进行中的 goal %s(%s),请先 complete/abandon"
106
- % (cur["goal_id"], cur["title"]))
107
- goal = {
108
- "goal_id": "g" + uuid.uuid4().hex[:12],
109
- "title": (title or "").strip(),
110
- "description": (description or "").strip(),
111
- "phase": "active",
112
- "revision": 1,
113
- "rounds_started": 0,
114
- "rounds_max": max(1, int(rounds_max)),
115
- "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
116
- "updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
117
- "metadata": dict(metadata or {}),
118
- }
119
- _GOALS[goal["goal_id"]] = goal
120
- _persist()
121
- return dict(goal)
122
-
123
-
124
- def update(goal_id: str, expected_revision: int, **changes) -> dict:
125
- """CAS 更新:expected_revision 不匹配即抛 StaleRevisionError。
126
-
127
- 允许变更的字段:title/description/phase/rounds_max/metadata。
128
- phase 只允许合法值;complete/abandoned 为终态不可再改回。
129
- """
130
- with _LOCK:
131
- g = _GOALS.get(goal_id)
132
- if not g:
133
- raise GoalNotFoundError(goal_id)
134
- if g["revision"] != expected_revision:
135
- raise StaleRevisionError(
136
- "%s 期望 rev %d,实际 %d" % (goal_id, expected_revision, g["revision"]))
137
- if g["phase"] in ("complete", "abandoned"):
138
- raise ValueError("goal 已终态(%s),不可再变更" % g["phase"])
139
- allowed = {"title", "description", "phase", "rounds_max",
140
- "rounds_started", "metadata"}
141
- for k, v in changes.items():
142
- if k not in allowed:
143
- raise ValueError("不可变更字段:%s" % k)
144
- if k == "phase" and not _valid_phase(v):
145
- raise ValueError("非法 phase:%s" % v)
146
- if k == "rounds_max":
147
- v = max(1, int(v))
148
- g[k] = v
149
- g["revision"] += 1
150
- g["updated_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
151
- _persist()
152
- return dict(g)
153
-
154
-
155
- def increment_rounds(goal_id: str, expected_revision: int) -> dict:
156
- """轮次 +1(Goal Round Driver 专用 CAS 入口)。"""
157
- with _LOCK:
158
- g = _GOALS.get(goal_id)
159
- if not g:
160
- raise GoalNotFoundError(goal_id)
161
- return update(goal_id, expected_revision, rounds_started=g["rounds_started"] + 1)
162
-
163
-
164
- def list_goals(limit: int = 50) -> list:
165
- with _LOCK:
166
- goals = sorted(_GOALS.values(),
167
- key=lambda g: g.get("created_at", ""), reverse=True)
1
+ # -*- coding: utf-8 -*-
2
+ """Goal 状态外置:跨轮目标(title/描述/phase/轮次)独立于 LLM 调用持久化。
3
+
4
+ 设计稿:docs/migration/03-state-externalization.md §2A。
5
+ 参考 dsh packages/goal/goal/:状态 100% 来自事件溯源(goal/change 事件),
6
+ 唯一当前 goal,revision 走 CAS 拒绝陈旧引用。
7
+
8
+ 与 planner.py 的关系:planner 继续负责「LLM 拆解」;GoalService 只管
9
+ 「目标状态」——谁都可以读(UI/续行驱动器/流水线),变更必须过 CAS。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import threading
15
+ import time
16
+ import uuid
17
+ from pathlib import Path
18
+
19
+ _LOCK = threading.RLock()
20
+ # goal_id -> dict(内存缓存;磁盘为唯一事实来源)
21
+ _GOALS = {}
22
+ _FILE = None # 由 init() 注入
23
+
24
+
25
+ def init(data_dir=None):
26
+ """注入存储路径并从磁盘加载。测试可传临时目录。"""
27
+ global _FILE, _GOALS
28
+ with _LOCK:
29
+ base = Path(data_dir) if data_dir else Path(_default_dir())
30
+ base.mkdir(parents=True, exist_ok=True)
31
+ _FILE = base / "goals.json"
32
+ _GOALS = {}
33
+ try:
34
+ data = json.loads(_FILE.read_text(encoding="utf-8"))
35
+ for g in data.get("goals") or []:
36
+ if isinstance(g, dict) and g.get("goal_id"):
37
+ _GOALS[g["goal_id"]] = g
38
+ except (OSError, json.JSONDecodeError):
39
+ pass
40
+
41
+
42
+ def _default_dir():
43
+ from . import paths
44
+ return str(paths.DATA_DIR)
45
+
46
+
47
+ def _persist():
48
+ """原子写盘(tmp + replace)。调用方必须已持有 _LOCK。"""
49
+ if _FILE is None:
50
+ return
51
+ tmp = _FILE.with_suffix(".tmp")
52
+ tmp.write_text(json.dumps({"goals": list(_GOALS.values())},
53
+ ensure_ascii=False, indent=2), encoding="utf-8")
54
+ tmp.replace(_FILE)
55
+
56
+
57
+ class StaleRevisionError(Exception):
58
+ """CAS 失败:调用方持有的是陈旧 revision。"""
59
+
60
+
61
+ class GoalExistsError(Exception):
62
+ """已有未完结的当前 goal,需先 complete/abandon。"""
63
+
64
+
65
+ class GoalNotFoundError(Exception):
66
+ pass
67
+
68
+
69
+ def _valid_phase(p):
70
+ return p in ("active", "paused", "complete", "abandoned")
71
+
72
+
73
+ def current():
74
+ """返回唯一「当前」goal(未 complete/abandoned 的最新一个);无则 None。"""
75
+ with _LOCK:
76
+ open_goals = [g for g in _GOALS.values()
77
+ if g.get("phase") in ("active", "paused")]
78
+ if not open_goals:
79
+ return None
80
+ return max(open_goals, key=lambda g: g.get("created_at", 0))
81
+
82
+
83
+ def get(goal_id: str):
84
+ with _LOCK:
85
+ g = _GOALS.get(goal_id)
86
+ return dict(g) if g else None
87
+
88
+
89
+ def create(title: str, description: str = "", *, rounds_max: int | None = None,
90
+ metadata: dict | None = None) -> dict:
91
+ """创建新 goal。已有未完结 goal 时拒绝(单一当前目标,仿 dsh)。
92
+
93
+ rounds_max 缺省读 settings_v2 orchestrator.max_goal_rounds(读不到回落 5)。"""
94
+ if rounds_max is None:
95
+ try:
96
+ from .settings_schema import get as ss_get, register_default_namespaces
97
+ register_default_namespaces()
98
+ rounds_max = int(ss_get("orchestrator", "max_goal_rounds") or 5)
99
+ except Exception:
100
+ rounds_max = 5
101
+ with _LOCK:
102
+ cur = current()
103
+ if cur is not None:
104
+ raise GoalExistsError(
105
+ "已有进行中的 goal %s(%s),请先 complete/abandon"
106
+ % (cur["goal_id"], cur["title"]))
107
+ goal = {
108
+ "goal_id": "g" + uuid.uuid4().hex[:12],
109
+ "title": (title or "").strip(),
110
+ "description": (description or "").strip(),
111
+ "phase": "active",
112
+ "revision": 1,
113
+ "rounds_started": 0,
114
+ "rounds_max": max(1, int(rounds_max)),
115
+ "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
116
+ "updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
117
+ "metadata": dict(metadata or {}),
118
+ }
119
+ _GOALS[goal["goal_id"]] = goal
120
+ _persist()
121
+ return dict(goal)
122
+
123
+
124
+ def update(goal_id: str, expected_revision: int, **changes) -> dict:
125
+ """CAS 更新:expected_revision 不匹配即抛 StaleRevisionError。
126
+
127
+ 允许变更的字段:title/description/phase/rounds_max/metadata。
128
+ phase 只允许合法值;complete/abandoned 为终态不可再改回。
129
+ """
130
+ with _LOCK:
131
+ g = _GOALS.get(goal_id)
132
+ if not g:
133
+ raise GoalNotFoundError(goal_id)
134
+ if g["revision"] != expected_revision:
135
+ raise StaleRevisionError(
136
+ "%s 期望 rev %d,实际 %d" % (goal_id, expected_revision, g["revision"]))
137
+ if g["phase"] in ("complete", "abandoned"):
138
+ raise ValueError("goal 已终态(%s),不可再变更" % g["phase"])
139
+ allowed = {"title", "description", "phase", "rounds_max",
140
+ "rounds_started", "metadata"}
141
+ for k, v in changes.items():
142
+ if k not in allowed:
143
+ raise ValueError("不可变更字段:%s" % k)
144
+ if k == "phase" and not _valid_phase(v):
145
+ raise ValueError("非法 phase:%s" % v)
146
+ if k == "rounds_max":
147
+ v = max(1, int(v))
148
+ g[k] = v
149
+ g["revision"] += 1
150
+ g["updated_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
151
+ _persist()
152
+ return dict(g)
153
+
154
+
155
+ def increment_rounds(goal_id: str, expected_revision: int) -> dict:
156
+ """轮次 +1(Goal Round Driver 专用 CAS 入口)。"""
157
+ with _LOCK:
158
+ g = _GOALS.get(goal_id)
159
+ if not g:
160
+ raise GoalNotFoundError(goal_id)
161
+ return update(goal_id, expected_revision, rounds_started=g["rounds_started"] + 1)
162
+
163
+
164
+ def list_goals(limit: int = 50) -> list:
165
+ with _LOCK:
166
+ goals = sorted(_GOALS.values(),
167
+ key=lambda g: g.get("created_at", ""), reverse=True)
168
168
  return [dict(g) for g in goals[:limit]]