codebee 0.1.10 → 0.1.11
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/CHANGELOG.md +14 -0
- package/README.md +8 -6
- package/app/core/aiflavor.py +51 -0
- package/app/core/automation.py +9 -0
- package/app/core/gitmod.py +4 -0
- package/app/core/jobs.py +115 -13
- package/app/core/manager.py +49 -0
- package/app/core/modelhub.py +10 -1
- package/app/core/paths.py +1 -0
- package/app/core/pipeline.py +282 -1
- package/app/core/publish/__init__.py +7 -0
- package/app/core/publish/auto.py +367 -0
- package/app/core/publish/browser.py +410 -0
- package/app/core/publish/fanqie.py +98 -0
- package/app/core/publish/flow.py +281 -0
- package/app/core/publish/ledger.py +198 -0
- package/app/core/publish/manager.py +427 -0
- package/app/core/publish/qimao.py +97 -0
- package/app/core/publish/ws.py +139 -0
- package/app/core/settings.py +18 -3
- package/app/core/store.py +24 -2
- package/app/main.py +174 -0
- package/app/ui/app.js +325 -26
- package/app/ui/i18n.js +4 -1
- package/app/ui/index.html +5 -3
- package/app/ui/style.css +64 -0
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -19,7 +19,7 @@ import re
|
|
|
19
19
|
import threading
|
|
20
20
|
import time
|
|
21
21
|
|
|
22
|
-
from . import catalog, history, jobs, manager, modelhub, mocks, planner, registry, router, runner, skills, store, usage
|
|
22
|
+
from . import aiflavor, catalog, history, jobs, manager, modelhub, mocks, planner, registry, router, runner, skills, store, usage
|
|
23
23
|
from . import builtin_agent
|
|
24
24
|
from . import diagnostics
|
|
25
25
|
from . import paths as paths_mod
|
|
@@ -730,6 +730,118 @@ def _format_issues(review_json, verify_pass, verify_failed_note):
|
|
|
730
730
|
|
|
731
731
|
# ---------------------------------------------------------------- 代码流水线
|
|
732
732
|
|
|
733
|
+
def _shortstat_files(diffstat_line):
|
|
734
|
+
"""` 3 files changed, 10 insertions(+)` → 文件数(解析失败给大数排最后)。"""
|
|
735
|
+
import re as _re
|
|
736
|
+
m = _re.search(r"(\d+) files? changed", diffstat_line or "")
|
|
737
|
+
return int(m.group(1)) if m else 9999
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _code_bestof(run, task, impl, difficulty, ev):
|
|
741
|
+
"""代码任务 Best-of-N(借鉴 orca 并行 worktree 择优)。启用条件:best_of≥2、
|
|
742
|
+
无续会话、非手动指定实现者、工作目录是 git 仓库。
|
|
743
|
+
|
|
744
|
+
按基线(git_rev 或 HEAD)建 N 个临时 worktree → 各路并行跑实现子任务链 →
|
|
745
|
+
各跑验证命令 → 择优(验证通过 > 改动文件更少)→ 胜者 diff 应用回主工作区。
|
|
746
|
+
任一环节失败一律返回 False,由调用方回落原单路实现(绝不因赛马挡任务)。"""
|
|
747
|
+
from . import gitmod
|
|
748
|
+
run_id = run["id"]
|
|
749
|
+
n = max(2, min(3, int(task.get("best_of") or 1)))
|
|
750
|
+
wd = task["workdir"]
|
|
751
|
+
if not gitmod._git(wd, "rev-parse", "--is-inside-work-tree")["ok"]:
|
|
752
|
+
return False
|
|
753
|
+
base = task.get("git_rev") or ""
|
|
754
|
+
if base and not gitmod.valid_rev(base):
|
|
755
|
+
return False
|
|
756
|
+
base_rev = (gitmod._git(wd, "rev-parse", base)["stdout"].strip() if base
|
|
757
|
+
else gitmod._git(wd, "rev-parse", "HEAD")["stdout"].strip())
|
|
758
|
+
if not base_rev:
|
|
759
|
+
return False
|
|
760
|
+
wts = []
|
|
761
|
+
results = {}
|
|
762
|
+
|
|
763
|
+
def _cleanup():
|
|
764
|
+
for k, br, wt_path in wts:
|
|
765
|
+
gitmod._git(wd, "worktree", "remove", "--force", wt_path, timeout=60)
|
|
766
|
+
gitmod._git(wd, "branch", "-D", br, timeout=60)
|
|
767
|
+
|
|
768
|
+
try:
|
|
769
|
+
import tempfile
|
|
770
|
+
race_root = os.path.join(tempfile.gettempdir(), "codebee-race", run_id)
|
|
771
|
+
os.makedirs(race_root, exist_ok=True)
|
|
772
|
+
for k in range(n):
|
|
773
|
+
br = "codebee-race-%s-%d" % (run_id, k)
|
|
774
|
+
wt_path = os.path.join(race_root, "v%d" % k)
|
|
775
|
+
r = gitmod._git(wd, "worktree", "add", "--detach", wt_path, base_rev, timeout=120)
|
|
776
|
+
if not r["ok"]:
|
|
777
|
+
raise RuntimeError(r["stderr"] or "worktree add 失败")
|
|
778
|
+
wts.append((k, br, wt_path))
|
|
779
|
+
|
|
780
|
+
subtasks = [{"title": task.get("title") or task.get("goal") or "实现",
|
|
781
|
+
"detail": task.get("goal") or ""}]
|
|
782
|
+
|
|
783
|
+
def _race_one(k, wt_path):
|
|
784
|
+
agt_b = modelhub.bind_agent(impl, difficulty)
|
|
785
|
+
res = None
|
|
786
|
+
for i, sub in enumerate(subtasks):
|
|
787
|
+
prompt = (CODE_IMPL_PROMPT
|
|
788
|
+
.replace("__GOAL__", task["goal"])
|
|
789
|
+
.replace("__SUBTASK__", sub["detail"] if sub["detail"] else sub["title"])
|
|
790
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
791
|
+
.replace("__VERIFY_HINT__", _verify_hint(task)))
|
|
792
|
+
res = _run_step(run_id, "race%d-impl" % k, agt_b, prompt, wt_path,
|
|
793
|
+
readonly=False, ev=ev,
|
|
794
|
+
note="赛马路 %d/%d(worktree 隔离)" % (k + 1, n),
|
|
795
|
+
images=_task_images(task, wd), require_tools=True)
|
|
796
|
+
if not res["ok"]:
|
|
797
|
+
break
|
|
798
|
+
v_pass = bool(res and res["ok"])
|
|
799
|
+
if v_pass and task.get("verify_command"):
|
|
800
|
+
r = runner.run_process(shell_cmd=task["verify_command"], cwd=wt_path,
|
|
801
|
+
timeout=600, cancel_event=ev)
|
|
802
|
+
v_pass = bool(r["ok"])
|
|
803
|
+
diffstat = (gitmod._git(wt_path, "diff", "--shortstat", base_rev)["stdout"] or "").strip()
|
|
804
|
+
results[k] = {"ok": bool(res and res["ok"]), "verify": v_pass,
|
|
805
|
+
"wt": wt_path, "diffstat": diffstat}
|
|
806
|
+
|
|
807
|
+
threads = []
|
|
808
|
+
for k, br, wt_path in wts:
|
|
809
|
+
th = threading.Thread(target=_race_one, args=(k, wt_path),
|
|
810
|
+
name="codebestof-%s-%d" % (run_id, k), daemon=True)
|
|
811
|
+
threads.append(th)
|
|
812
|
+
th.start()
|
|
813
|
+
for th in threads:
|
|
814
|
+
th.join(3600)
|
|
815
|
+
_check_cancel(ev)
|
|
816
|
+
|
|
817
|
+
usable = [r for k, r in sorted(results.items()) if r["ok"] and r["verify"]]
|
|
818
|
+
if not usable:
|
|
819
|
+
return False # 全败:回落单路实现(保持原有换将/报错行为)
|
|
820
|
+
usable.sort(key=lambda r: _shortstat_files(r["diffstat"]))
|
|
821
|
+
winner = usable[0]
|
|
822
|
+
diff = gitmod._git(winner["wt"], "diff", base_rev, timeout=120)
|
|
823
|
+
if diff["ok"] and diff["stdout"].strip():
|
|
824
|
+
apply_r = runner.run_process(
|
|
825
|
+
argv=["git", "apply", "--whitespace=nowarn"],
|
|
826
|
+
cwd=wd, timeout=60, stdin_text=diff["stdout"])
|
|
827
|
+
if not apply_r["ok"]:
|
|
828
|
+
return False
|
|
829
|
+
store.update_run(run_id, bestof={
|
|
830
|
+
"kind": "code-worktree", "candidates": len(results),
|
|
831
|
+
"winner_files": _shortstat_files(winner["diffstat"]),
|
|
832
|
+
"diffstat": winner["diffstat"],
|
|
833
|
+
})
|
|
834
|
+
return True
|
|
835
|
+
except Exception:
|
|
836
|
+
log.warning("code bestof aborted run=%s", run_id, exc_info=True)
|
|
837
|
+
return False
|
|
838
|
+
finally:
|
|
839
|
+
try:
|
|
840
|
+
_cleanup()
|
|
841
|
+
except Exception:
|
|
842
|
+
pass
|
|
843
|
+
|
|
844
|
+
|
|
733
845
|
def _run_code(run, task, agents, ev, stats, mode):
|
|
734
846
|
run_id = run["id"]
|
|
735
847
|
workdir = task["workdir"]
|
|
@@ -842,6 +954,13 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
842
954
|
ok, res = _run_one(impl_agent)
|
|
843
955
|
if ok:
|
|
844
956
|
return True
|
|
957
|
+
# 单路实现失败:Best-of-N 赛马兜底(借鉴 orca worktree 择优)——多路并行
|
|
958
|
+
# 各自 worktree 隔离实现+验证,胜者 diff 回主工作区;失败回落走换将
|
|
959
|
+
if (mode == "auto" and impl_agent.get("mode") == "real"
|
|
960
|
+
and max(1, min(3, int(task.get("best_of") or 1))) >= 2
|
|
961
|
+
and resume_ctx is None
|
|
962
|
+
and _code_bestof(run, task, impl_agent, difficulty, ev)):
|
|
963
|
+
return True
|
|
845
964
|
# 实现步失败不立刻判死:2026-09-16 实测配额烧干时 5 连跑全在同一条 CLI 上
|
|
846
965
|
# 失败收场,而健康的 opencode 一直在旁观望——跨 CLI 换将重试一次
|
|
847
966
|
# (mode=manual 尊重用户指定,不换)。
|
|
@@ -957,6 +1076,7 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
957
1076
|
lines.append("(无)")
|
|
958
1077
|
lines += ["", "## 评审总评", "", review_json.get("summary", ""), ""]
|
|
959
1078
|
store.write_report(run_id, "\n".join(lines))
|
|
1079
|
+
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
960
1080
|
store.update_run(run_id, status="done", verdict=verdict,
|
|
961
1081
|
summary="代码任务%s(验证%s / 评审%s%s)" % (
|
|
962
1082
|
"通过" if overall_pass else "未通过",
|
|
@@ -1224,6 +1344,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1224
1344
|
if last_text:
|
|
1225
1345
|
report += ["## 最近一轮输出", "", last_text[-5000:], ""]
|
|
1226
1346
|
store.write_report(run_id, "\n".join(report))
|
|
1347
|
+
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
1227
1348
|
store.update_run(run_id, status="done", verdict=verdict,
|
|
1228
1349
|
summary="直连完成(%d 轮):%s" % (turns, last_text[:160]),
|
|
1229
1350
|
ended_at=_now())
|
|
@@ -1292,6 +1413,8 @@ def _tpl(task, key, default):
|
|
|
1292
1413
|
|
|
1293
1414
|
BIBLE_FILE = "story-bible.md"
|
|
1294
1415
|
_BIBLE_MAX_CHARS = 20000
|
|
1416
|
+
MODULES_FILE = "plot-modules.md"
|
|
1417
|
+
_MODULES_MAX_CHARS = 12000
|
|
1295
1418
|
|
|
1296
1419
|
# 评审视角播种(dev-3.0 式 bug hunters):N 个评审各领一个深挖镜头,
|
|
1297
1420
|
# 避免全员盯着同一处。按评审序号取模分配——同一评审每轮同一镜头,
|
|
@@ -1321,6 +1444,23 @@ def _story_bible(workdir):
|
|
|
1321
1444
|
"与其冲突处以圣经为准)\n\n" + txt)
|
|
1322
1445
|
|
|
1323
1446
|
|
|
1447
|
+
def _plot_modules(workdir):
|
|
1448
|
+
"""剧情模块库(oh-story 拆文沉淀式):工作目录里的 plot-modules.md
|
|
1449
|
+
(可复用的桥段/冲突/爽点/名场面素材模块),作者手工维护,每章起草与
|
|
1450
|
+
评审前自动注入。不存在/为空返回 ""——约定式功能,零配置零噪音。"""
|
|
1451
|
+
p = os.path.abspath(os.path.join(str(workdir or ""), MODULES_FILE))
|
|
1452
|
+
if not _inside(workdir, p) or not os.path.isfile(p):
|
|
1453
|
+
return ""
|
|
1454
|
+
try:
|
|
1455
|
+
txt = _read_text_any_enc(p)[:_MODULES_MAX_CHARS].strip()
|
|
1456
|
+
except OSError:
|
|
1457
|
+
return ""
|
|
1458
|
+
if not txt:
|
|
1459
|
+
return ""
|
|
1460
|
+
return ("## 剧情模块库(plot-modules.md:可复用的桥段/冲突/爽点素材模块,"
|
|
1461
|
+
"鼓励化用,不要照抄原句)\n\n" + txt)
|
|
1462
|
+
|
|
1463
|
+
|
|
1324
1464
|
def _critic_lens(critics, agent):
|
|
1325
1465
|
"""该评审的专属视角;单评审/手动指定时不播种(无从轮换,也别稀释注意力)。"""
|
|
1326
1466
|
try:
|
|
@@ -1521,6 +1661,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1521
1661
|
start = int(serial.get("start_chapter") or 1)
|
|
1522
1662
|
# 故事圣经:工作目录里的 story-bible.md,整个 run 内字节稳定(前缀缓存友好)
|
|
1523
1663
|
bible = _story_bible(workdir)
|
|
1664
|
+
# 剧情模块库:plot-modules.md(拆文沉淀的可复用素材模块)并入同一注入块,
|
|
1665
|
+
# 同样要求 run 内字节稳定;无模块库时零噪音
|
|
1666
|
+
mods = _plot_modules(workdir)
|
|
1667
|
+
if mods:
|
|
1668
|
+
bible = (bible + "\n\n" + mods) if bible else mods
|
|
1524
1669
|
|
|
1525
1670
|
|
|
1526
1671
|
def crit_prompt_for(text, note=""):
|
|
@@ -1689,6 +1834,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1689
1834
|
critic_sids = {} # §07 T1.1:每评审的会话 id(第 2 轮复用,前缀走缓存读)
|
|
1690
1835
|
race_cj = None # 变体赛马已评审胜者:直接作为第 1 轮结果,不重评
|
|
1691
1836
|
race_scored = 0
|
|
1837
|
+
race_losers = [] # 赛马败稿文本(收卷前留存):首轮修订时提炼败者精华
|
|
1692
1838
|
|
|
1693
1839
|
reuse = i in done_set and os.path.exists(os.path.join(workdir, ch_file))
|
|
1694
1840
|
if reuse:
|
|
@@ -1914,6 +2060,13 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1914
2060
|
error="第 %d 章赛马收卷失败: %r" % (i, e), ended_at=_now())
|
|
1915
2061
|
return
|
|
1916
2062
|
for v in scored_variants[1:]:
|
|
2063
|
+
# 败者精华:删除前留存文本,供首轮修订提炼(懒调用)
|
|
2064
|
+
try:
|
|
2065
|
+
race_losers.append({
|
|
2066
|
+
"variant": v["variant"],
|
|
2067
|
+
"text": (_read_text_any_enc(os.path.join(workdir, v["file"])) or "")[:8000]})
|
|
2068
|
+
except OSError:
|
|
2069
|
+
pass
|
|
1917
2070
|
try:
|
|
1918
2071
|
os.remove(os.path.join(workdir, v["file"]))
|
|
1919
2072
|
except OSError:
|
|
@@ -1966,6 +2119,27 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1966
2119
|
crit_lines = ["- %s:%.1f(章阈值 %.1f)" % (d, means[d], threshold_ch) for d in dims]
|
|
1967
2120
|
crit_lines += ["- [%s] %s" % (x.get("dim", "?"), str(x.get("note", ""))[:140])
|
|
1968
2121
|
for x in majors]
|
|
2122
|
+
if rnd == 1 and race_losers and impl.get("mode") == "real":
|
|
2123
|
+
# 赛马败者精华回收(连载版):一次选择器调用提炼落选稿优点,
|
|
2124
|
+
# 与评审意见一并喂给首轮修订;失败静默不影响修订
|
|
2125
|
+
try:
|
|
2126
|
+
cands = "### 胜者稿(已选定)\n\n%s" % _read_chapter(workdir, i)[:8000]
|
|
2127
|
+
for li, lo in enumerate(race_losers, 1):
|
|
2128
|
+
cands += "\n\n### 落选稿 %d\n\n%s" % (li, lo["text"])
|
|
2129
|
+
sel_prompt = (BESTOF_SELECTOR_PROMPT
|
|
2130
|
+
.replace("__GOAL__", task["goal"])
|
|
2131
|
+
.replace("__CANDIDATES__", cands))
|
|
2132
|
+
sel = _run_step(run_id, "race-select-c%d" % i,
|
|
2133
|
+
modelhub.bind_agent(critics[0] if critics else impl, difficulty),
|
|
2134
|
+
sel_prompt, step_wd, readonly=True, ev=ev,
|
|
2135
|
+
note="赛马败者精华提炼")
|
|
2136
|
+
_selcj = runner.extract_json(sel.get("text") or "")
|
|
2137
|
+
_imp = str((_selcj or {}).get("improvements") or "").strip()
|
|
2138
|
+
if _imp:
|
|
2139
|
+
crit_lines.append("- [赛马精华] 终审从落选候选稿提炼出值得吸收的优点:%s"
|
|
2140
|
+
% _imp[:400])
|
|
2141
|
+
except Exception:
|
|
2142
|
+
pass
|
|
1969
2143
|
if impl.get("mode") == "mock":
|
|
1970
2144
|
step, log_abs = store.add_step(run_id, "revise-c%d" % i, impl["id"],
|
|
1971
2145
|
impl.get("label"))
|
|
@@ -2198,6 +2372,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2198
2372
|
else:
|
|
2199
2373
|
lines.append("(无 major 问题)")
|
|
2200
2374
|
store.write_report(run_id, "\n".join(lines))
|
|
2375
|
+
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
2201
2376
|
store.update_run(run_id, status="done", verdict=verdict,
|
|
2202
2377
|
summary="连载任务%s(%s,约 %d 字,综合 %.1f)" % (
|
|
2203
2378
|
"达标" if publishable else "未达标", scope_txt,
|
|
@@ -2429,6 +2604,11 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2429
2604
|
_tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
|
|
2430
2605
|
.replace("__DIMKEYS__", dimkey)
|
|
2431
2606
|
.replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
|
|
2607
|
+
# AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
|
|
2608
|
+
# 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
|
|
2609
|
+
_aiflavor_line = aiflavor.report_line(manuscript)
|
|
2610
|
+
if _aiflavor_line:
|
|
2611
|
+
crit_prompt += "\n\n## 确定性检测结果(供评审参考)\n" + _aiflavor_line
|
|
2432
2612
|
for agent in critics:
|
|
2433
2613
|
role = "critique-r%d" % r
|
|
2434
2614
|
if agent.get("mode") == "mock":
|
|
@@ -2538,6 +2718,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2538
2718
|
lines.append("(无)")
|
|
2539
2719
|
lines += ["", "## 稿件位置", "", "`%s`" % ms_path, ""]
|
|
2540
2720
|
store.write_report(run_id, "\n".join(lines))
|
|
2721
|
+
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
2541
2722
|
store.update_run(run_id, status="done", verdict=verdict,
|
|
2542
2723
|
summary="评审任务%s(综合 %.1f)" % ("达标" if publishable else "未达标", overall),
|
|
2543
2724
|
ended_at=_now())
|
|
@@ -2593,6 +2774,103 @@ def _run_serial_qa(run, task, agents, ev):
|
|
|
2593
2774
|
error="答疑失败(执行/评审链不可用)——" + ";".join(errors[-3:]))
|
|
2594
2775
|
|
|
2595
2776
|
|
|
2777
|
+
def _write_task_spec(task, workdir):
|
|
2778
|
+
"""任务规格落盘 .codebee/spec.md(借鉴 agent-orchestrator 的 .spec/PROMPT.md 与
|
|
2779
|
+
planning-with-files 的文件化计划):任务定义随工作目录留存、随任务分支版本化,
|
|
2780
|
+
追话/复盘/续跑时可见原始意图。失败静默返回空串——规格文件永远不能挡住任务执行。"""
|
|
2781
|
+
try:
|
|
2782
|
+
from pathlib import Path as _P
|
|
2783
|
+
root = _P(workdir).resolve()
|
|
2784
|
+
target = (root / ".codebee" / "spec.md").resolve()
|
|
2785
|
+
if root not in target.parents: # 守卫:spec 必须落在工作目录内(../、symlink 出逃弃写)
|
|
2786
|
+
return ""
|
|
2787
|
+
os.makedirs(str(target.parent), exist_ok=True)
|
|
2788
|
+
lines = [
|
|
2789
|
+
"# 任务规格", "",
|
|
2790
|
+
"- 标题:%s" % (task.get("title") or ""),
|
|
2791
|
+
"- 类型:%s" % (task.get("type") or ""),
|
|
2792
|
+
"- 创建:%s" % (task.get("created_at") or ""),
|
|
2793
|
+
"- 目标:%s" % str(task.get("goal") or "").replace("\n", " "),
|
|
2794
|
+
]
|
|
2795
|
+
if task.get("context"):
|
|
2796
|
+
lines.append("- 背景:%s" % str(task["context"]).replace("\n", " "))
|
|
2797
|
+
if task.get("difficulty"):
|
|
2798
|
+
lines.append("- 难度:%s" % task["difficulty"])
|
|
2799
|
+
if task.get("mode"):
|
|
2800
|
+
lines.append("- 路由模式:%s" % task["mode"])
|
|
2801
|
+
label = {"rounds": "评审轮数", "threshold": "发布阈值", "best_of": "赛马候选数"}
|
|
2802
|
+
for k in ("rounds", "threshold", "best_of"):
|
|
2803
|
+
if task.get(k) is not None:
|
|
2804
|
+
lines.append("- %s:%s" % (label[k], task[k]))
|
|
2805
|
+
if task.get("rubric"):
|
|
2806
|
+
lines.append("- 评审维度:%s" % "、".join(task["rubric"]))
|
|
2807
|
+
if task.get("serial"):
|
|
2808
|
+
s = task["serial"]
|
|
2809
|
+
lines.append("- 连载:%s 章 × %s 字(赛马变体 %s)"
|
|
2810
|
+
% (s.get("chapters"), s.get("words_per_chapter"), s.get("variants", 1)))
|
|
2811
|
+
if task.get("verify_command"):
|
|
2812
|
+
lines.append("- 验证命令:`%s`" % task["verify_command"])
|
|
2813
|
+
if not str(target).startswith(str(root) + os.sep): # sink 侧复检:路径必须仍在工作目录内
|
|
2814
|
+
return ""
|
|
2815
|
+
with open(str(target), "w", encoding="utf-8") as f:
|
|
2816
|
+
f.write("\n".join(lines) + "\n")
|
|
2817
|
+
return str(target)
|
|
2818
|
+
except Exception:
|
|
2819
|
+
return ""
|
|
2820
|
+
|
|
2821
|
+
|
|
2822
|
+
def _write_task_evidence(run_id, task, workdir, summary_lines):
|
|
2823
|
+
"""验证证据持久化(借鉴 gsd-pi 的 validation evidence):run 收尾把确定性
|
|
2824
|
+
证据追加进 .codebee/evidence.md——验证命令结果、评审分数、AI 味检测等。
|
|
2825
|
+
与 spec.md(任务意图)呼应成「任务档案」;失败静默,绝不挡收尾。"""
|
|
2826
|
+
if not summary_lines:
|
|
2827
|
+
return ""
|
|
2828
|
+
try:
|
|
2829
|
+
spec_dir = os.path.join(workdir, ".codebee")
|
|
2830
|
+
os.makedirs(spec_dir, exist_ok=True)
|
|
2831
|
+
path = os.path.join(spec_dir, "evidence.md")
|
|
2832
|
+
header_needed = not os.path.exists(path)
|
|
2833
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
2834
|
+
if header_needed:
|
|
2835
|
+
f.write("# 验证证据(每次运行追加一节)\n\n")
|
|
2836
|
+
f.write("## %s · run %s\n\n" % (_now(), run_id))
|
|
2837
|
+
for ln in summary_lines:
|
|
2838
|
+
f.write("- %s\n" % str(ln)[:300])
|
|
2839
|
+
f.write("\n")
|
|
2840
|
+
return path
|
|
2841
|
+
except Exception:
|
|
2842
|
+
return ""
|
|
2843
|
+
|
|
2844
|
+
|
|
2845
|
+
def _evidence_lines_from_run(run_id, task):
|
|
2846
|
+
"""从 run 步骤与 verdict 提取证据行(确定性事实,不抄模型输出)。"""
|
|
2847
|
+
run = store.get_run(run_id) or {}
|
|
2848
|
+
lines = []
|
|
2849
|
+
for s in run.get("steps") or []:
|
|
2850
|
+
role = str(s.get("role") or "")
|
|
2851
|
+
if role == "verify":
|
|
2852
|
+
lines.append("验证命令 `%s` → %s%s" % (
|
|
2853
|
+
task.get("verify_command") or "", s.get("status"),
|
|
2854
|
+
"(%s)" % s.get("summary") if s.get("summary") else ""))
|
|
2855
|
+
verdict = run.get("verdict") or {}
|
|
2856
|
+
if verdict:
|
|
2857
|
+
means = verdict.get("scores") or {}
|
|
2858
|
+
if means:
|
|
2859
|
+
lines.append("评审均分:%s(阈值 %s,%s)" % (
|
|
2860
|
+
"、".join("%s %.1f" % (d, v) for d, v in means.items()),
|
|
2861
|
+
verdict.get("threshold"),
|
|
2862
|
+
"达标" if verdict.get("publishable") else "未达标"))
|
|
2863
|
+
if verdict.get("overall") is not None:
|
|
2864
|
+
lines.append("综合分 %.1f / %d 轮" % (verdict.get("overall") or 0.0,
|
|
2865
|
+
verdict.get("rounds_used") or 0))
|
|
2866
|
+
bestof = run.get("bestof")
|
|
2867
|
+
if bestof:
|
|
2868
|
+
lines.append("赛马:%s(胜者 %s)" % (
|
|
2869
|
+
bestof.get("kind") or "内容候选",
|
|
2870
|
+
bestof.get("pick", bestof.get("winner_files", "?"))))
|
|
2871
|
+
return lines
|
|
2872
|
+
|
|
2873
|
+
|
|
2596
2874
|
def execute_run(run_id):
|
|
2597
2875
|
run = store.get_run(run_id)
|
|
2598
2876
|
if not run:
|
|
@@ -2627,6 +2905,9 @@ def execute_run(run_id):
|
|
|
2627
2905
|
store.update_run(run_id, git=gitinfo)
|
|
2628
2906
|
# 任务分支裁决状态:新一轮 run 产生新分支内容,重置回「待裁决」
|
|
2629
2907
|
store.set_task_git_state(task["id"], "isolated")
|
|
2908
|
+
# 任务规格文件化(借鉴 planning-with-files/agent-orchestrator):任何任务都在
|
|
2909
|
+
# 工作目录留一份 .codebee/spec.md——原始意图可见、随任务分支版本化
|
|
2910
|
+
_write_task_spec(task, task["workdir"])
|
|
2630
2911
|
agents = _agents()
|
|
2631
2912
|
global _CURRENT_AGENTS
|
|
2632
2913
|
_CURRENT_AGENTS = agents
|