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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+ """一键发布:CDP 驱动平台后台(番茄/七猫)建书与章节发布。
3
+
4
+ 分层:ws.py(最小 WebSocket)→ browser.py(CDP 驱动)→ ledger.py(台账)
5
+ → 平台流程(数据驱动的步骤表,selector 可外置覆盖)→ manager.py(状态机)。
6
+ """
7
+ from . import browser, ledger # noqa: F401