codebee 0.1.10 → 0.1.12
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 +21 -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 +316 -2
- 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 +5 -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"]
|
|
@@ -779,6 +891,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
779
891
|
plan = {"source": "manual", "steps": [{"title": "实现任务", "detail": task["goal"]}]}
|
|
780
892
|
store.update_run(run_id, plan=plan, difficulty=difficulty)
|
|
781
893
|
subtasks = plan["steps"]
|
|
894
|
+
# 计划落盘(planning-with-files):磁盘上的计划与 spec/evidence 同居任务档案
|
|
895
|
+
_write_task_plan(task, workdir, plan)
|
|
782
896
|
|
|
783
897
|
# ---- 评审者(有会话延续时评审者仍用新鲜上下文,避免偏见)
|
|
784
898
|
if mode == "auto":
|
|
@@ -811,12 +925,20 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
811
925
|
except Exception:
|
|
812
926
|
pass
|
|
813
927
|
for i, sub in enumerate(subtasks):
|
|
928
|
+
# 子任务进度注入(planning-with-files 的每轮注入计划头):多子任务时
|
|
929
|
+
# 让实现者知道全局位置与已完成项,防止长链目标漂移;单子任务零噪音
|
|
930
|
+
prog = ""
|
|
931
|
+
if len(subtasks) > 1:
|
|
932
|
+
done_titles = "、".join(
|
|
933
|
+
(s.get("title") or "")[:40] for s in subtasks[:i]) or "(无)"
|
|
934
|
+
prog = ("\n\n## 计划进度(第 %d/%d 项)\n已完成:%s。当前接着做下面这一项,"
|
|
935
|
+
"不要重做已完成的。" % (i + 1, len(subtasks), done_titles))
|
|
814
936
|
prompt = (CODE_IMPL_PROMPT
|
|
815
937
|
.replace("__GOAL__", task["goal"])
|
|
816
938
|
.replace("__SUBTASK__",
|
|
817
939
|
sub["detail"] if sub["detail"] else sub["title"])
|
|
818
940
|
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
819
|
-
.replace("__VERIFY_HINT__", _verify_hint(task)))
|
|
941
|
+
.replace("__VERIFY_HINT__", _verify_hint(task))) + prog
|
|
820
942
|
role = "implement" if len(subtasks) == 1 else "implement-%d/%d" % (i + 1, len(subtasks))
|
|
821
943
|
res = _run_step(run_id, role, agt_b, prompt, step_wd,
|
|
822
944
|
readonly=False, ev=ev,
|
|
@@ -842,6 +964,13 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
842
964
|
ok, res = _run_one(impl_agent)
|
|
843
965
|
if ok:
|
|
844
966
|
return True
|
|
967
|
+
# 单路实现失败:Best-of-N 赛马兜底(借鉴 orca worktree 择优)——多路并行
|
|
968
|
+
# 各自 worktree 隔离实现+验证,胜者 diff 回主工作区;失败回落走换将
|
|
969
|
+
if (mode == "auto" and impl_agent.get("mode") == "real"
|
|
970
|
+
and max(1, min(3, int(task.get("best_of") or 1))) >= 2
|
|
971
|
+
and resume_ctx is None
|
|
972
|
+
and _code_bestof(run, task, impl_agent, difficulty, ev)):
|
|
973
|
+
return True
|
|
845
974
|
# 实现步失败不立刻判死:2026-09-16 实测配额烧干时 5 连跑全在同一条 CLI 上
|
|
846
975
|
# 失败收场,而健康的 opencode 一直在旁观望——跨 CLI 换将重试一次
|
|
847
976
|
# (mode=manual 尊重用户指定,不换)。
|
|
@@ -957,6 +1086,7 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
957
1086
|
lines.append("(无)")
|
|
958
1087
|
lines += ["", "## 评审总评", "", review_json.get("summary", ""), ""]
|
|
959
1088
|
store.write_report(run_id, "\n".join(lines))
|
|
1089
|
+
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
960
1090
|
store.update_run(run_id, status="done", verdict=verdict,
|
|
961
1091
|
summary="代码任务%s(验证%s / 评审%s%s)" % (
|
|
962
1092
|
"通过" if overall_pass else "未通过",
|
|
@@ -1224,6 +1354,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1224
1354
|
if last_text:
|
|
1225
1355
|
report += ["## 最近一轮输出", "", last_text[-5000:], ""]
|
|
1226
1356
|
store.write_report(run_id, "\n".join(report))
|
|
1357
|
+
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
1227
1358
|
store.update_run(run_id, status="done", verdict=verdict,
|
|
1228
1359
|
summary="直连完成(%d 轮):%s" % (turns, last_text[:160]),
|
|
1229
1360
|
ended_at=_now())
|
|
@@ -1292,6 +1423,8 @@ def _tpl(task, key, default):
|
|
|
1292
1423
|
|
|
1293
1424
|
BIBLE_FILE = "story-bible.md"
|
|
1294
1425
|
_BIBLE_MAX_CHARS = 20000
|
|
1426
|
+
MODULES_FILE = "plot-modules.md"
|
|
1427
|
+
_MODULES_MAX_CHARS = 12000
|
|
1295
1428
|
|
|
1296
1429
|
# 评审视角播种(dev-3.0 式 bug hunters):N 个评审各领一个深挖镜头,
|
|
1297
1430
|
# 避免全员盯着同一处。按评审序号取模分配——同一评审每轮同一镜头,
|
|
@@ -1321,6 +1454,23 @@ def _story_bible(workdir):
|
|
|
1321
1454
|
"与其冲突处以圣经为准)\n\n" + txt)
|
|
1322
1455
|
|
|
1323
1456
|
|
|
1457
|
+
def _plot_modules(workdir):
|
|
1458
|
+
"""剧情模块库(oh-story 拆文沉淀式):工作目录里的 plot-modules.md
|
|
1459
|
+
(可复用的桥段/冲突/爽点/名场面素材模块),作者手工维护,每章起草与
|
|
1460
|
+
评审前自动注入。不存在/为空返回 ""——约定式功能,零配置零噪音。"""
|
|
1461
|
+
p = os.path.abspath(os.path.join(str(workdir or ""), MODULES_FILE))
|
|
1462
|
+
if not _inside(workdir, p) or not os.path.isfile(p):
|
|
1463
|
+
return ""
|
|
1464
|
+
try:
|
|
1465
|
+
txt = _read_text_any_enc(p)[:_MODULES_MAX_CHARS].strip()
|
|
1466
|
+
except OSError:
|
|
1467
|
+
return ""
|
|
1468
|
+
if not txt:
|
|
1469
|
+
return ""
|
|
1470
|
+
return ("## 剧情模块库(plot-modules.md:可复用的桥段/冲突/爽点素材模块,"
|
|
1471
|
+
"鼓励化用,不要照抄原句)\n\n" + txt)
|
|
1472
|
+
|
|
1473
|
+
|
|
1324
1474
|
def _critic_lens(critics, agent):
|
|
1325
1475
|
"""该评审的专属视角;单评审/手动指定时不播种(无从轮换,也别稀释注意力)。"""
|
|
1326
1476
|
try:
|
|
@@ -1521,6 +1671,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1521
1671
|
start = int(serial.get("start_chapter") or 1)
|
|
1522
1672
|
# 故事圣经:工作目录里的 story-bible.md,整个 run 内字节稳定(前缀缓存友好)
|
|
1523
1673
|
bible = _story_bible(workdir)
|
|
1674
|
+
# 剧情模块库:plot-modules.md(拆文沉淀的可复用素材模块)并入同一注入块,
|
|
1675
|
+
# 同样要求 run 内字节稳定;无模块库时零噪音
|
|
1676
|
+
mods = _plot_modules(workdir)
|
|
1677
|
+
if mods:
|
|
1678
|
+
bible = (bible + "\n\n" + mods) if bible else mods
|
|
1524
1679
|
|
|
1525
1680
|
|
|
1526
1681
|
def crit_prompt_for(text, note=""):
|
|
@@ -1689,6 +1844,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1689
1844
|
critic_sids = {} # §07 T1.1:每评审的会话 id(第 2 轮复用,前缀走缓存读)
|
|
1690
1845
|
race_cj = None # 变体赛马已评审胜者:直接作为第 1 轮结果,不重评
|
|
1691
1846
|
race_scored = 0
|
|
1847
|
+
race_losers = [] # 赛马败稿文本(收卷前留存):首轮修订时提炼败者精华
|
|
1692
1848
|
|
|
1693
1849
|
reuse = i in done_set and os.path.exists(os.path.join(workdir, ch_file))
|
|
1694
1850
|
if reuse:
|
|
@@ -1914,6 +2070,13 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1914
2070
|
error="第 %d 章赛马收卷失败: %r" % (i, e), ended_at=_now())
|
|
1915
2071
|
return
|
|
1916
2072
|
for v in scored_variants[1:]:
|
|
2073
|
+
# 败者精华:删除前留存文本,供首轮修订提炼(懒调用)
|
|
2074
|
+
try:
|
|
2075
|
+
race_losers.append({
|
|
2076
|
+
"variant": v["variant"],
|
|
2077
|
+
"text": (_read_text_any_enc(os.path.join(workdir, v["file"])) or "")[:8000]})
|
|
2078
|
+
except OSError:
|
|
2079
|
+
pass
|
|
1917
2080
|
try:
|
|
1918
2081
|
os.remove(os.path.join(workdir, v["file"]))
|
|
1919
2082
|
except OSError:
|
|
@@ -1966,6 +2129,27 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1966
2129
|
crit_lines = ["- %s:%.1f(章阈值 %.1f)" % (d, means[d], threshold_ch) for d in dims]
|
|
1967
2130
|
crit_lines += ["- [%s] %s" % (x.get("dim", "?"), str(x.get("note", ""))[:140])
|
|
1968
2131
|
for x in majors]
|
|
2132
|
+
if rnd == 1 and race_losers and impl.get("mode") == "real":
|
|
2133
|
+
# 赛马败者精华回收(连载版):一次选择器调用提炼落选稿优点,
|
|
2134
|
+
# 与评审意见一并喂给首轮修订;失败静默不影响修订
|
|
2135
|
+
try:
|
|
2136
|
+
cands = "### 胜者稿(已选定)\n\n%s" % _read_chapter(workdir, i)[:8000]
|
|
2137
|
+
for li, lo in enumerate(race_losers, 1):
|
|
2138
|
+
cands += "\n\n### 落选稿 %d\n\n%s" % (li, lo["text"])
|
|
2139
|
+
sel_prompt = (BESTOF_SELECTOR_PROMPT
|
|
2140
|
+
.replace("__GOAL__", task["goal"])
|
|
2141
|
+
.replace("__CANDIDATES__", cands))
|
|
2142
|
+
sel = _run_step(run_id, "race-select-c%d" % i,
|
|
2143
|
+
modelhub.bind_agent(critics[0] if critics else impl, difficulty),
|
|
2144
|
+
sel_prompt, step_wd, readonly=True, ev=ev,
|
|
2145
|
+
note="赛马败者精华提炼")
|
|
2146
|
+
_selcj = runner.extract_json(sel.get("text") or "")
|
|
2147
|
+
_imp = str((_selcj or {}).get("improvements") or "").strip()
|
|
2148
|
+
if _imp:
|
|
2149
|
+
crit_lines.append("- [赛马精华] 终审从落选候选稿提炼出值得吸收的优点:%s"
|
|
2150
|
+
% _imp[:400])
|
|
2151
|
+
except Exception:
|
|
2152
|
+
pass
|
|
1969
2153
|
if impl.get("mode") == "mock":
|
|
1970
2154
|
step, log_abs = store.add_step(run_id, "revise-c%d" % i, impl["id"],
|
|
1971
2155
|
impl.get("label"))
|
|
@@ -2198,6 +2382,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2198
2382
|
else:
|
|
2199
2383
|
lines.append("(无 major 问题)")
|
|
2200
2384
|
store.write_report(run_id, "\n".join(lines))
|
|
2385
|
+
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
2201
2386
|
store.update_run(run_id, status="done", verdict=verdict,
|
|
2202
2387
|
summary="连载任务%s(%s,约 %d 字,综合 %.1f)" % (
|
|
2203
2388
|
"达标" if publishable else "未达标", scope_txt,
|
|
@@ -2429,6 +2614,11 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2429
2614
|
_tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
|
|
2430
2615
|
.replace("__DIMKEYS__", dimkey)
|
|
2431
2616
|
.replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
|
|
2617
|
+
# AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
|
|
2618
|
+
# 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
|
|
2619
|
+
_aiflavor_line = aiflavor.report_line(manuscript)
|
|
2620
|
+
if _aiflavor_line:
|
|
2621
|
+
crit_prompt += "\n\n## 确定性检测结果(供评审参考)\n" + _aiflavor_line
|
|
2432
2622
|
for agent in critics:
|
|
2433
2623
|
role = "critique-r%d" % r
|
|
2434
2624
|
if agent.get("mode") == "mock":
|
|
@@ -2538,6 +2728,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2538
2728
|
lines.append("(无)")
|
|
2539
2729
|
lines += ["", "## 稿件位置", "", "`%s`" % ms_path, ""]
|
|
2540
2730
|
store.write_report(run_id, "\n".join(lines))
|
|
2731
|
+
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
2541
2732
|
store.update_run(run_id, status="done", verdict=verdict,
|
|
2542
2733
|
summary="评审任务%s(综合 %.1f)" % ("达标" if publishable else "未达标", overall),
|
|
2543
2734
|
ended_at=_now())
|
|
@@ -2593,6 +2784,126 @@ def _run_serial_qa(run, task, agents, ev):
|
|
|
2593
2784
|
error="答疑失败(执行/评审链不可用)——" + ";".join(errors[-3:]))
|
|
2594
2785
|
|
|
2595
2786
|
|
|
2787
|
+
def _write_task_spec(task, workdir):
|
|
2788
|
+
"""任务规格落盘 .codebee/spec.md(借鉴 agent-orchestrator 的 .spec/PROMPT.md 与
|
|
2789
|
+
planning-with-files 的文件化计划):任务定义随工作目录留存、随任务分支版本化,
|
|
2790
|
+
追话/复盘/续跑时可见原始意图。失败静默返回空串——规格文件永远不能挡住任务执行。"""
|
|
2791
|
+
try:
|
|
2792
|
+
from pathlib import Path as _P
|
|
2793
|
+
root = _P(workdir).resolve()
|
|
2794
|
+
target = (root / ".codebee" / "spec.md").resolve()
|
|
2795
|
+
if root not in target.parents: # 守卫:spec 必须落在工作目录内(../、symlink 出逃弃写)
|
|
2796
|
+
return ""
|
|
2797
|
+
os.makedirs(str(target.parent), exist_ok=True)
|
|
2798
|
+
lines = [
|
|
2799
|
+
"# 任务规格", "",
|
|
2800
|
+
"- 标题:%s" % (task.get("title") or ""),
|
|
2801
|
+
"- 类型:%s" % (task.get("type") or ""),
|
|
2802
|
+
"- 创建:%s" % (task.get("created_at") or ""),
|
|
2803
|
+
"- 目标:%s" % str(task.get("goal") or "").replace("\n", " "),
|
|
2804
|
+
]
|
|
2805
|
+
if task.get("context"):
|
|
2806
|
+
lines.append("- 背景:%s" % str(task["context"]).replace("\n", " "))
|
|
2807
|
+
if task.get("difficulty"):
|
|
2808
|
+
lines.append("- 难度:%s" % task["difficulty"])
|
|
2809
|
+
if task.get("mode"):
|
|
2810
|
+
lines.append("- 路由模式:%s" % task["mode"])
|
|
2811
|
+
label = {"rounds": "评审轮数", "threshold": "发布阈值", "best_of": "赛马候选数"}
|
|
2812
|
+
for k in ("rounds", "threshold", "best_of"):
|
|
2813
|
+
if task.get(k) is not None:
|
|
2814
|
+
lines.append("- %s:%s" % (label[k], task[k]))
|
|
2815
|
+
if task.get("rubric"):
|
|
2816
|
+
lines.append("- 评审维度:%s" % "、".join(task["rubric"]))
|
|
2817
|
+
if task.get("serial"):
|
|
2818
|
+
s = task["serial"]
|
|
2819
|
+
lines.append("- 连载:%s 章 × %s 字(赛马变体 %s)"
|
|
2820
|
+
% (s.get("chapters"), s.get("words_per_chapter"), s.get("variants", 1)))
|
|
2821
|
+
if task.get("verify_command"):
|
|
2822
|
+
lines.append("- 验证命令:`%s`" % task["verify_command"])
|
|
2823
|
+
if not str(target).startswith(str(root) + os.sep): # sink 侧复检:路径必须仍在工作目录内
|
|
2824
|
+
return ""
|
|
2825
|
+
with open(str(target), "w", encoding="utf-8") as f:
|
|
2826
|
+
f.write("\n".join(lines) + "\n")
|
|
2827
|
+
return str(target)
|
|
2828
|
+
except Exception:
|
|
2829
|
+
return ""
|
|
2830
|
+
|
|
2831
|
+
|
|
2832
|
+
def _write_task_evidence(run_id, task, workdir, summary_lines):
|
|
2833
|
+
"""验证证据持久化(借鉴 gsd-pi 的 validation evidence):run 收尾把确定性
|
|
2834
|
+
证据追加进 .codebee/evidence.md——验证命令结果、评审分数、AI 味检测等。
|
|
2835
|
+
与 spec.md(任务意图)呼应成「任务档案」;失败静默,绝不挡收尾。"""
|
|
2836
|
+
if not summary_lines:
|
|
2837
|
+
return ""
|
|
2838
|
+
try:
|
|
2839
|
+
spec_dir = os.path.join(workdir, ".codebee")
|
|
2840
|
+
os.makedirs(spec_dir, exist_ok=True)
|
|
2841
|
+
path = os.path.join(spec_dir, "evidence.md")
|
|
2842
|
+
header_needed = not os.path.exists(path)
|
|
2843
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
2844
|
+
if header_needed:
|
|
2845
|
+
f.write("# 验证证据(每次运行追加一节)\n\n")
|
|
2846
|
+
f.write("## %s · run %s\n\n" % (_now(), run_id))
|
|
2847
|
+
for ln in summary_lines:
|
|
2848
|
+
f.write("- %s\n" % str(ln)[:300])
|
|
2849
|
+
f.write("\n")
|
|
2850
|
+
return path
|
|
2851
|
+
except Exception:
|
|
2852
|
+
return ""
|
|
2853
|
+
|
|
2854
|
+
|
|
2855
|
+
def _evidence_lines_from_run(run_id, task):
|
|
2856
|
+
"""从 run 步骤与 verdict 提取证据行(确定性事实,不抄模型输出)。"""
|
|
2857
|
+
run = store.get_run(run_id) or {}
|
|
2858
|
+
lines = []
|
|
2859
|
+
for s in run.get("steps") or []:
|
|
2860
|
+
role = str(s.get("role") or "")
|
|
2861
|
+
if role == "verify":
|
|
2862
|
+
lines.append("验证命令 `%s` → %s%s" % (
|
|
2863
|
+
task.get("verify_command") or "", s.get("status"),
|
|
2864
|
+
"(%s)" % s.get("summary") if s.get("summary") else ""))
|
|
2865
|
+
verdict = run.get("verdict") or {}
|
|
2866
|
+
if verdict:
|
|
2867
|
+
means = verdict.get("scores") or {}
|
|
2868
|
+
if means:
|
|
2869
|
+
lines.append("评审均分:%s(阈值 %s,%s)" % (
|
|
2870
|
+
"、".join("%s %.1f" % (d, v) for d, v in means.items()),
|
|
2871
|
+
verdict.get("threshold"),
|
|
2872
|
+
"达标" if verdict.get("publishable") else "未达标"))
|
|
2873
|
+
if verdict.get("overall") is not None:
|
|
2874
|
+
lines.append("综合分 %.1f / %d 轮" % (verdict.get("overall") or 0.0,
|
|
2875
|
+
verdict.get("rounds_used") or 0))
|
|
2876
|
+
bestof = run.get("bestof")
|
|
2877
|
+
if bestof:
|
|
2878
|
+
lines.append("赛马:%s(胜者 %s)" % (
|
|
2879
|
+
bestof.get("kind") or "内容候选",
|
|
2880
|
+
bestof.get("pick", bestof.get("winner_files", "?"))))
|
|
2881
|
+
return lines
|
|
2882
|
+
|
|
2883
|
+
|
|
2884
|
+
def _write_task_plan(task, workdir, plan):
|
|
2885
|
+
"""计划落盘 .codebee/task_plan.md(planning-with-files 精髓:计划活在磁盘上,
|
|
2886
|
+
/clear、压缩、崩溃、续跑都不丢)。与 spec.md/evidence.md 同居任务档案;
|
|
2887
|
+
失败静默——计划文件永远不能挡住任务执行。"""
|
|
2888
|
+
try:
|
|
2889
|
+
from pathlib import Path as _P
|
|
2890
|
+
steps = (plan or {}).get("steps") or []
|
|
2891
|
+
if not steps:
|
|
2892
|
+
return "" # 无步骤不产空计划文件
|
|
2893
|
+
root = _P(workdir).resolve()
|
|
2894
|
+
target = (root / ".codebee" / "task_plan.md").resolve()
|
|
2895
|
+
if root not in target.parents:
|
|
2896
|
+
return ""
|
|
2897
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
2898
|
+
lines = ["# 任务计划", "", "来源:%s" % (plan or {}).get("source", "?"), ""]
|
|
2899
|
+
for i, s in enumerate((plan or {}).get("steps") or [], 1):
|
|
2900
|
+
lines.append("%d. %s" % (i, str(s.get("detail") or s.get("title") or "")[:200]))
|
|
2901
|
+
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
2902
|
+
return str(target)
|
|
2903
|
+
except Exception:
|
|
2904
|
+
return ""
|
|
2905
|
+
|
|
2906
|
+
|
|
2596
2907
|
def execute_run(run_id):
|
|
2597
2908
|
run = store.get_run(run_id)
|
|
2598
2909
|
if not run:
|
|
@@ -2627,6 +2938,9 @@ def execute_run(run_id):
|
|
|
2627
2938
|
store.update_run(run_id, git=gitinfo)
|
|
2628
2939
|
# 任务分支裁决状态:新一轮 run 产生新分支内容,重置回「待裁决」
|
|
2629
2940
|
store.set_task_git_state(task["id"], "isolated")
|
|
2941
|
+
# 任务规格文件化(借鉴 planning-with-files/agent-orchestrator):任何任务都在
|
|
2942
|
+
# 工作目录留一份 .codebee/spec.md——原始意图可见、随任务分支版本化
|
|
2943
|
+
_write_task_spec(task, task["workdir"])
|
|
2630
2944
|
agents = _agents()
|
|
2631
2945
|
global _CURRENT_AGENTS
|
|
2632
2946
|
_CURRENT_AGENTS = agents
|