codebee 0.1.95 → 0.1.97

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 (144) hide show
  1. package/CHANGELOG.md +733 -720
  2. package/LICENSE +21 -21
  3. package/README.md +516 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +118 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +246 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +2055 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +648 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mcp_client.py +200 -29
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +181 -189
  49. package/app/core/openapi.py +177 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +319 -59
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +84 -0
  55. package/app/core/portguard.py +111 -111
  56. package/app/core/presence.py +77 -77
  57. package/app/core/preview.py +293 -293
  58. package/app/core/project_memory.py +139 -0
  59. package/app/core/prompt_guard.py +78 -78
  60. package/app/core/publish/__init__.py +7 -7
  61. package/app/core/publish/auto.py +106 -22
  62. package/app/core/publish/browser.py +694 -655
  63. package/app/core/publish/chaprows.py +138 -0
  64. package/app/core/publish/fanqie.py +212 -165
  65. package/app/core/publish/flow.py +832 -640
  66. package/app/core/publish/flows-fanqie-calibrated.json +361 -336
  67. package/app/core/publish/ledger.py +51 -2
  68. package/app/core/publish/manager.py +1150 -1118
  69. package/app/core/publish/qimao.py +176 -176
  70. package/app/core/publish/volumes.py +108 -0
  71. package/app/core/publish/ws.py +139 -139
  72. package/app/core/quality_gate.py +194 -194
  73. package/app/core/redact.py +38 -38
  74. package/app/core/repeat_guard.py +129 -129
  75. package/app/core/retrieval.py +137 -137
  76. package/app/core/router.py +398 -398
  77. package/app/core/runner.py +2239 -2238
  78. package/app/core/selfupdate.py +384 -384
  79. package/app/core/session_log.py +161 -161
  80. package/app/core/sessions.py +312 -312
  81. package/app/core/settings.py +33 -5
  82. package/app/core/settings_schema.py +282 -271
  83. package/app/core/share_page.py +114 -114
  84. package/app/core/skill_scan.py +139 -139
  85. package/app/core/skillpacks/fanqie-novel.md +8 -6
  86. package/app/core/skillpacks/market/character-bible.md +66 -66
  87. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  88. package/app/core/skillpacks/market/git-workflow.md +57 -57
  89. package/app/core/skillpacks/market/release-notes.md +72 -72
  90. package/app/core/skillpacks/market/weekly-report.md +71 -71
  91. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  92. package/app/core/skillpacks/qimao-signing.md +14 -11
  93. package/app/core/skills.py +32 -21
  94. package/app/core/step_runner.py +132 -132
  95. package/app/core/store.py +24 -0
  96. package/app/core/story_tracking.py +275 -275
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tool_outputs.py +107 -0
  102. package/app/core/tracing.py +207 -144
  103. package/app/core/upstream.py +41 -41
  104. package/app/core/usage.py +1038 -1038
  105. package/app/core/volumes.py +389 -389
  106. package/app/core/webhooks.py +89 -0
  107. package/app/core/wxdigest.py +865 -858
  108. package/app/core/zentao.py +2692 -2692
  109. package/app/main.py +179 -10
  110. package/app/mcp_server.py +244 -244
  111. package/app/pick_dialog.py +110 -110
  112. package/app/ui/app.js +563 -21
  113. package/app/ui/artdiff.js +74 -74
  114. package/app/ui/assets/community-qr.png +0 -0
  115. package/app/ui/assets/donate-alipay.jpg +0 -0
  116. package/app/ui/assets/donate-qr.jpg +0 -0
  117. package/app/ui/assets/workbench-reference.png +0 -0
  118. package/app/ui/board.css +451 -666
  119. package/app/ui/board.html +165 -162
  120. package/app/ui/board.js +509 -305
  121. package/app/ui/hive3d.js +242 -0
  122. package/app/ui/i18n.js +125 -9
  123. package/app/ui/icons/bee.svg +79 -79
  124. package/app/ui/index.html +60 -3
  125. package/app/ui/manifest.json +16 -16
  126. package/app/ui/qrcode.js +2297 -2297
  127. package/app/ui/style.css +189 -9
  128. package/bin/tutti.js +147 -147
  129. package/package.json +49 -48
  130. package/tools/wx_reader.py +261 -261
  131. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  132. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  133. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  134. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  135. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  136. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  137. package/app/.playwright-cli/run-detail-after.png +0 -0
  138. package/app/output/_createbook_body.json +0 -1
  139. package/app/output/_probe_fq_click.py +0 -50
  140. package/app/output/_probe_fq_form.py +0 -42
  141. package/app/output/_probe_fq_modal.py +0 -55
  142. package/app/output/_probe_fq_points.py +0 -71
  143. package/app/output/_probe_fq_submit.py +0 -34
  144. package/app/output/_probe_fq_watch.py +0 -62
@@ -19,7 +19,7 @@ import re
19
19
  import threading
20
20
  import time
21
21
 
22
- from . import aiflavor, attachments, branching, catalog, chaptersafe, defectretro, dispatch_log, flows, history, hooks, jobs, knowledge, manager, modelhub, mocks, novel_quality, paihang, planner, quality_gate, registry, router, runner, skills, store, task_compile, usage, volumes
22
+ from . import aiflavor, agent_context, attachments, branching, catalog, chaptersafe, checkpoints, defectretro, dispatch_log, flows, history, hooks, jobs, knowledge, manager, modelhub, mocks, novel_quality, paihang, planner, quality_gate, registry, router, runner, skills, store, task_compile, usage, volumes
23
23
  from . import builtin_agent
24
24
  from . import diagnostics
25
25
  from . import paths as paths_mod
@@ -229,10 +229,12 @@ def _resume_workdir(resume_ctx, fallback):
229
229
 
230
230
 
231
231
  def _compaction_enabled():
232
- """Phase 2 灰度开关:环境变量 TUTTI_COMPACTION=1 或设置页
233
- settings_v2 orchestrator.compaction.enabled 任一开启即生效(默认关)。"""
234
- if os.environ.get("TUTTI_COMPACTION") == "1":
232
+ """环境变量可显式覆盖设置页开关;未设置时读取 settings_v2。"""
233
+ env_value = os.environ.get("TUTTI_COMPACTION", "").strip().lower()
234
+ if env_value in ("1", "true", "yes", "on"):
235
235
  return True
236
+ if env_value in ("0", "false", "no", "off"):
237
+ return False
236
238
  try:
237
239
  from .settings_schema import get as ss_get, register_default_namespaces
238
240
  register_default_namespaces()
@@ -442,7 +444,39 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
442
444
  agent.get("label", agent["id"]), note=note,
443
445
  model=agent.get("model") or "",
444
446
  provider=_binding_brief(agent))
447
+ try:
448
+ checkpoints.start(
449
+ run_id, step["n"], role, prompt,
450
+ input_data={"workdir": workdir, "readonly": readonly,
451
+ "agent": agent.get("id")},
452
+ attempt=len((store.get_run(run_id) or {}).get("steps") or []))
453
+ except Exception:
454
+ pass
445
455
  start = time.time()
456
+ # Third-party CLI backends have their own sandbox semantics, but do not
457
+ # consume CodeBee's per-task policy. Fail closed for narrowed policies
458
+ # instead of treating a prompt or workdir as an OS-level boundary.
459
+ sandbox_error = ""
460
+ try:
461
+ run_row = store.get_run(run_id) or {}
462
+ task_row = store.get_task(run_row.get("task_id")) if run_row.get("task_id") else {}
463
+ if agent.get("mode") != "mock":
464
+ if not task_row:
465
+ sandbox_error = "无法读取任务沙箱策略,外部智能体执行已拒绝"
466
+ else:
467
+ from . import policy
468
+ sandbox_error = _external_sandbox_block_reason(
469
+ agent, workdir, task_row.get("sandbox") or {})
470
+ except Exception:
471
+ # A missing task record is not permission to synthesize a wider policy.
472
+ sandbox_error = "无法读取任务沙箱策略,外部智能体执行已拒绝"
473
+ if sandbox_error:
474
+ res = {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
475
+ "tokens": 0, "usage": None, "error": sandbox_error,
476
+ "raw": {"exit_code": None, "sandbox_blocked": True},
477
+ "kind": agent.get("kind", "generic"), "model": agent.get("model")}
478
+ _finish_step_result(run_id, step, res, role, agent, start)
479
+ return res
446
480
  if dead_binding:
447
481
  from .error_codes import ErrorCode
448
482
  res = {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
@@ -484,11 +518,47 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
484
518
  if directive_block:
485
519
  prompt = directive_block + "\n\n---\n\n" + prompt
486
520
  effective_prompt = (guard["reminder"] + "\n\n---\n\n" + prompt) if guard["reminder"] else prompt
521
+ git_checkpoint = None
522
+ try:
523
+ git_checkpoint = checkpoints.begin_git_step(run_id, workdir)
524
+ except Exception:
525
+ git_checkpoint = {"ok": False, "error": "文件检查点准备失败,外部智能体未启动"}
526
+ if not git_checkpoint.get("ok"):
527
+ res = {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
528
+ "tokens": 0, "usage": None,
529
+ "error": git_checkpoint.get("error") or "文件检查点准备失败",
530
+ "raw": {"exit_code": None, "checkpoint_blocked": True},
531
+ "kind": agent.get("kind", "generic"), "model": agent.get("model")}
532
+ _finish_step_result(run_id, step, res, role, agent, start)
533
+ return res
487
534
  res = _spawn_step(session_run_id=run_id, role=role, agent=agent,
488
535
  prompt=effective_prompt, workdir=workdir, readonly=readonly,
489
536
  ev=ev, timeout=timeout, resume=resume, step=step,
490
537
  log_abs=log_abs, images=images, require_tools=require_tools,
491
538
  deadline=deadline)
539
+ checkpoint_warning = git_checkpoint.get("warning", "")
540
+ if git_checkpoint.get("supported"):
541
+ try:
542
+ finished_checkpoint = checkpoints.finish_git_step(
543
+ run_id, workdir, git_checkpoint.get("head"),
544
+ git_checkpoint.get("before_paths") or [])
545
+ checkpoint_warning = finished_checkpoint.get("warning") or ""
546
+ if not finished_checkpoint.get("ok"):
547
+ checkpoint_warning = finished_checkpoint.get("warning") or "外部智能体文件快照未能完整记录"
548
+ except Exception:
549
+ checkpoint_warning = "外部智能体文件快照记录失败;本步骤改动可能无法恢复"
550
+ if checkpoint_warning:
551
+ res["checkpoint_warning"] = checkpoint_warning
552
+ if res.get("ok"):
553
+ res["text"] = (res.get("text") or "") + "\n\n(检查点范围提示:%s)" % checkpoint_warning
554
+ elif res.get("error"):
555
+ res["error"] = res["error"] + ";" + checkpoint_warning
556
+ if log_abs:
557
+ try:
558
+ with log_abs.open("a", encoding="utf-8") as handle:
559
+ handle.write("\n[检查点] %s\n" % checkpoint_warning)
560
+ except OSError:
561
+ pass
492
562
  # 先收尾再查取消:取消时进程已被 run_process 杀停,若先抛 Cancelled,
493
563
  # 步骤记录会永远停在「运行中」变僵尸(与 _run_verify 的顺序对齐)
494
564
  _finish_step_result(run_id, step, res, role, agent, start)
@@ -500,6 +570,32 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
500
570
  return res
501
571
 
502
572
 
573
+ def _external_sandbox_block_reason(agent, workdir, sandbox):
574
+ """Reject task policies that this CLI launcher cannot enforce.
575
+
576
+ Codex is currently the only supported external runner with an OS-backed
577
+ workspace write sandbox. Other CLI integrations have no equivalent boundary
578
+ in this launcher and must use the in-process agent/tool executor instead.
579
+ """
580
+ if (agent or {}).get("mode") == "mock":
581
+ return ""
582
+ from . import policy
583
+ if (agent or {}).get("kind") != "codex":
584
+ return "沙箱拒绝:该外部 CLI 没有经验证的 OS 文件隔离;请改用内置 Agent"
585
+ root = os.path.realpath(workdir or ".")
586
+ normalized = policy.normalize_sandbox(sandbox, root)
587
+ roots = [os.path.realpath(x) for x in normalized.get("allowed_roots") or []]
588
+ if normalized.get("network") is False:
589
+ return "沙箱拒绝:该 CLI 后端未接入网络隔离,拒绝运行网络受限任务"
590
+ if roots != [root]:
591
+ return "沙箱拒绝:该 CLI 后端未接入允许目录隔离,拒绝运行目录受限任务"
592
+ if normalized.get("disabled_tools"):
593
+ return "权限拒绝:该 CLI 后端无法执行 CodeBee 工具禁用策略,拒绝运行"
594
+ if normalized.get("env_allowlist"):
595
+ return "沙箱拒绝:该 CLI 后端未接入环境变量白名单,拒绝运行受限任务"
596
+ return ""
597
+
598
+
503
599
  def _create_task_from_builtin(payload):
504
600
  """Create and enqueue a task requested by the direct conversation tool."""
505
601
  task = store.create_task(payload)
@@ -579,7 +675,8 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
579
675
  deadline=deadline, cancel_event=ev, log=_log, images=images,
580
676
  on_reason=_on_reason, on_stream=_on_stream,
581
677
  on_activity=_on_activity,
582
- task_creator=_create_task_from_builtin)
678
+ task_creator=_create_task_from_builtin,
679
+ sandbox=task.get("sandbox"), checkpoint_run_id=run_id)
583
680
  if followups and res.get("ok"):
584
681
  clean, fups = _parse_followups(res.get("text") or "")
585
682
  if fups:
@@ -779,6 +876,17 @@ def _finish_step_result(run_id, step, res, role, agent, start):
779
876
  # 思考过程(内置智能体流式抓取):落进步骤记录,对话气泡折叠展示
780
877
  thinking=res.get("reasoning") or None,
781
878
  partial=bool(res.get("partial")))
879
+ try:
880
+ checkpoint_status = ("unknown" if raw.get("unknown") else
881
+ "timeout" if status == "timeout" else
882
+ "cancelled" if status == "cancelled" else
883
+ "done" if status == "done" else "failed")
884
+ checkpoints.finish(
885
+ run_id, step["n"], checkpoint_status,
886
+ error=res.get("error") or "", output=res.get("text") or "",
887
+ metadata={"role": role, "agent": agent.get("id") or ""})
888
+ except Exception:
889
+ pass
782
890
  # 错误台账:失败/超时各记一条结构化记录(遥测与诊断包的数据源)。
783
891
  # 用户主动取消不入账——那不是产品问题;detail 只存脱敏后的失败摘录。
784
892
  if status in ("failed", "timeout"):
@@ -1442,6 +1550,28 @@ def _run_code(run, task, agents, ev, stats, mode):
1442
1550
  if project_memory:
1443
1551
  task = dict(task, context=(task.get("context") or "") + "\n\n" + project_memory)
1444
1552
 
1553
+ # 断点简报(B):重试继承上一遍失败/中断 run 的现场,注入规划器与实现者
1554
+ # 共用的 __CONTEXT__——「走到哪、改了什么、还剩什么」不再从零重建上下文。
1555
+ _handoff = run.get("handoff") or {}
1556
+ _brief = _handoff_brief(_handoff)
1557
+ if _brief:
1558
+ task = dict(task, context=(task.get("context") or "") + "\n\n" + _brief)
1559
+ # 断点会话(A):上一遍实现者的 CLI 会话 id,implement_all 里按人复用。
1560
+ _inh_session = run.get("inherit_session") or {}
1561
+
1562
+ def _keep_sid(agent_row, agent_b, r):
1563
+ """记录最后一次实现的会话 id:run 内 fix 轮复用 + 落盘 run 供重试跨 run 续会话。"""
1564
+ sid = _resume_sid(agent_b, r.get("sid"))
1565
+ if sid:
1566
+ impl_sid[0] = sid
1567
+ try:
1568
+ store.update_run(run_id, impl_session={
1569
+ "agent": agent_row.get("id"), "session": sid,
1570
+ "workdir": workdir})
1571
+ except Exception:
1572
+ pass
1573
+ return sid
1574
+
1445
1575
  # ---- 规划。低风险短任务直接把目标作为单步计划,省掉一次独立模型调用;
1446
1576
  # 附件或较长的用户背景仍先规划,避免快速路径漏读约束。
1447
1577
  fast_path = (mode == "fast" or
@@ -1507,11 +1637,20 @@ def _run_code(run, task, agents, ev, stats, mode):
1507
1637
 
1508
1638
  def implement_all(impl_agent, prefix_note):
1509
1639
  nonlocal reviewer
1510
- # 会话延续只对原实现者有效;换将后新智能体没有该会话,必须丢弃
1640
+ # 会话延续只对原实现者有效;换将后新智能体没有该会话,必须丢弃。
1641
+ # 优先级:任务级 resume(用户显式指定)> 断点继承会话(A:跨 run 续跑,
1642
+ # 仅同一实现者 + 同一工作目录 + 该 CLI 支持续会话时复用)
1643
+ inh_sid = ""
1644
+ if (not resume_ctx and _inh_session.get("session")
1645
+ and _inh_session.get("agent") == impl_agent["id"]
1646
+ and _same_workdir(_inh_session.get("workdir") or "", workdir)
1647
+ and _resume_sid(impl_agent, _inh_session.get("session"))):
1648
+ inh_sid = _inh_session["session"]
1511
1649
  use_resume = (resume_ctx["session"]
1512
- if (resume_ctx and impl_agent["id"] == resume_ctx["agent"]["id"]) else None)
1650
+ if (resume_ctx and impl_agent["id"] == resume_ctx["agent"]["id"])
1651
+ else (inh_sid or None))
1513
1652
  # 续会话时 CLI 要在会话所属项目目录下启动,否则定位不到会话
1514
- step_wd = _resume_workdir(resume_ctx, workdir) if use_resume else workdir
1653
+ step_wd = _resume_workdir(resume_ctx, workdir) if (resume_ctx and use_resume) else workdir
1515
1654
 
1516
1655
  def _run_one(agt):
1517
1656
  """用指定智能体跑全部子任务;返回 (ok, 最后一次 res)。"""
@@ -1557,10 +1696,20 @@ def _run_code(run, task, agents, ev, stats, mode):
1557
1696
  note=prefix_note if i == 0 else "",
1558
1697
  resume=use_resume, images=att_imgs,
1559
1698
  require_tools=True, swap_guard=swap_guard)
1560
- # §07 T1.1:记录最后一次实现的会话 id,fix 轮复用(会话内前缀走缓存读计价)
1561
- new_sid = _resume_sid(agt_b, res.get("sid"))
1562
- if new_sid:
1563
- impl_sid[0] = new_sid
1699
+ _keep_sid(agt, agt_b, res)
1700
+ # 断点继承会话失效兜底(A):会话可能已被 CLI 清理或跨机不可续。
1701
+ # 认证/配额类死因换会话也没用,不重试;其余丢弃继承会话原样重跑一次。
1702
+ if (not res["ok"] and inh_sid and i == 0 and use_resume == inh_sid
1703
+ and not (res.get("raw") or {}).get("cancelled")
1704
+ and not runner._auth_error(res.get("error") or "")
1705
+ and not runner._quota_error(res.get("error") or "")):
1706
+ res = _run_step(run_id, role, agt_b, prompt, step_wd,
1707
+ readonly=False, ev=ev,
1708
+ note=((prefix_note + ";") if prefix_note else "")
1709
+ + "继承会话续跑失败,已丢弃旧会话全新执行",
1710
+ resume=None, images=att_imgs,
1711
+ require_tools=True, swap_guard=swap_guard)
1712
+ _keep_sid(agt, agt_b, res)
1564
1713
  if agt.get("mode") == "mock" and res["ok"]:
1565
1714
  try:
1566
1715
  mock_path = os.path.abspath(os.path.join(workdir, "mock-impl.txt"))
@@ -1587,10 +1736,11 @@ def _run_code(run, task, agents, ev, stats, mode):
1587
1736
  error=auth_err, ended_at=_now())
1588
1737
  return False
1589
1738
  # 单路实现失败:Best-of-N 赛马兜底(借鉴 orca worktree 择优)——多路并行
1590
- # 各自 worktree 隔离实现+验证,胜者 diff 回主工作区;失败回落走换将
1739
+ # 各自 worktree 隔离实现+验证,胜者 diff 回主工作区;失败回落走换将。
1740
+ # 会话延续在场时跳过(N 路共用同一 CLI 会话会互相践踏,与连载同理)
1591
1741
  if (mode == "auto" and impl_agent.get("mode") == "real"
1592
1742
  and max(1, min(3, int(task.get("best_of") or 1))) >= 2
1593
- and resume_ctx is None
1743
+ and resume_ctx is None and not inh_sid
1594
1744
  and _code_bestof(run, task, impl_agent, difficulty, ev)):
1595
1745
  return True
1596
1746
  # 实现步失败不立刻判死:2026-09-16 实测配额烧干时 5 连跑全在同一条 CLI 上
@@ -1737,6 +1887,7 @@ def _run_code(run, task, agents, ev, stats, mode):
1737
1887
  note="自动修复第 %d 轮" % round_no,
1738
1888
  resume=resume_ctx["session"] if resume_ctx else impl_sid[0],
1739
1889
  require_tools=True)
1890
+ _keep_sid(impl, modelhub.bind_agent(impl, difficulty), res)
1740
1891
  if (res.get("raw") or {}).get("timed_out"):
1741
1892
  # 自动修复没有换将/重试兜底;超时后继续验收会把未完成的修复
1742
1893
  # 当作正常轮次,让 run 继续显示 running,最终还可能误标 done。
@@ -1854,7 +2005,7 @@ def _run_code(run, task, agents, ev, stats, mode):
1854
2005
  _mem_lines = ["改动文件:%s" % (_touched_str or "(无 diff)"),
1855
2006
  "验收:%s" % ("通过" if verify_pass else "未通过"),
1856
2007
  "修复轮数:%d" % (len(repairs) - 1)]
1857
- _write_project_memory(task, workdir, _mem_lines)
2008
+ _write_project_memory(task, workdir, _mem_lines, source_run=run_id)
1858
2009
  except Exception:
1859
2010
  pass
1860
2011
  # Stop gate(planning-with-files):收工时活计划若有未勾选项,摘要与 verdict
@@ -2692,8 +2843,8 @@ __VOLUME__
2692
2843
  - 撰写本书第 __I__ 章,把本章正文写入文件 `__FILE__`(直接写入该文件,只写本章)。文件必须以 UTF-8 编码保存:PowerShell 一律显式加 `-Encoding UTF8`(如 `Set-Content -Path __FILE__ -Encoding UTF8`),禁止依赖系统默认编码,否则中文会乱码。
2693
2844
  - 章节标题:__TITLE__
2694
2845
  - 剧情要点:__BEATS__
2695
- - 章末钩子:__HOOK__
2696
- - 本章爽点/情绪爆点:__HIGHLIGHT__(必须按特写镜头写厚:铺垫在前、放大在中、余波在后,用动作/五感/生理反应/环境反馈呈现,禁止一句话带过)
2846
+ - 章末收束/继续阅读理由(可自然收束或留白):__HOOK__
2847
+ - 本章主要情绪/戏剧变化:__HIGHLIGHT__(按其重要性决定篇幅和表现方式;不强制放大,不为满足标签伪造高潮)
2697
2848
  __LOCKED_CH__
2698
2849
  __STALE_NOTE__
2699
2850
  - 正文约 __WORDS__ 字,中文,直接开写正文(可含本章标题行)。
@@ -2732,23 +2883,23 @@ __STYLE__
2732
2883
 
2733
2884
  ## 签约返修原则
2734
2885
  - 统一叙述语域、句式和视角距离,不在白描、半文半白和网络梗之间跳变。
2735
- - 复杂情感必须落到动作、五感、生理反应和对话潜台词,禁止只写“很伤心/很愤怒”。
2736
- - 爽点/情绪爆点按特写镜头写厚:铺垫在前、放大在中、余波在后,禁止一句话带过。
2886
+ - 复杂情感需按其叙事分量呈现;动作、感官、对话、内心直述、环境或留白均可,避免与本稿风格不合的机械扩写。
2887
+ - 重要情绪/戏剧变化要有与其分量相称的呈现;避免概括带过,但不把每个变化都扩写成高潮。
2737
2888
  - 删除不改变局面的环境、解释和重复叙述;每段至少完成冲突、信息或人物选择之一。
2738
2889
  - 让主角主动做出选择并推动下一步,补足因果桥,不用旁白硬推剧情。
2739
2890
 
2740
2891
  ## 要求
2741
2892
  - 针对性解决所有 major 问题(含确定性文本信号指出的问题),保持与前后的剧情衔接;字数仍约 __WORDS__ 字。"""
2742
2893
 
2743
- SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全书各章已完稿,请从**全书整体**视角评审。
2894
+ SERIAL_GLOBAL_PROMPT = """你是独立小说质量评审(不要修改任何文件),不代表任何签约平台。全书各章已完稿,请从**全书整体**视角评审。
2744
2895
  请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
2745
2896
  {
2746
2897
  "scores": {"__DIMKEYS__"},
2747
2898
  "issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
2748
- "summary": "一句话总评:是否达到可签约水平"
2899
+ "summary": "一句话总评:所读范围内的文本优势、主要风险与未验证项;不得预测签约结果"
2749
2900
  }
2750
- 每个维度打 1-10 分,宁严勿宽。重点关注:主线一致性、人物弧光、开篇吸引力、文风统一、情感细腻度、情节推进与节奏控制。
2751
- 若是前 3 章,必须核对黄金一章/黄金三章的切入速度;若发现拖沓、硬推或情绪概括,给出可定位的 major 问题与原文引文。
2901
+ 每个维度打 1-10 分,按统一文本 rubric 与所读证据评估,不以“宁严勿宽”压分。重点关注:主线一致性、人物弧光、开篇吸引力、文风表现、情感表达、情节推进与节奏控制。
2902
+ 若是全书最初的前 3 章,可观察视角人物、故事承诺、开篇兑现和读者理解成本;固定字数点、钩子/爽点密度仅作参考,不是扣分门槛。发现问题须给可定位原文引文,并区分文本事实、解释和建议。
2752
2903
  major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
2753
2904
 
2754
2905
  ## 全书目标
@@ -2759,19 +2910,19 @@ __GOAL__
2759
2910
  __MANUSCRIPT__
2760
2911
  ---"""
2761
2912
 
2762
- # 独立签约评估(开篇闸门 + 2万字检查点共用):评分维度保持作者任务口径,
2763
- # 平台五维在这里按平台真实尺度单独核——驳回模板的高频话术逐条要判定。
2764
- SERIAL_SIGNING_EVAL_PROMPT = """你是签约评估编辑(不要修改任何文件)。请按免费阅读平台(番茄/七猫)签约评估的口径评审下面的稿件__NOTE__。
2913
+ # 独立文本质量评估(开篇闸门 + 项目检查点共用):沿用稳定 rubric,
2914
+ # 不代表平台决定;按原文证据检查常见拒稿问题,不将经验观察点变成硬门槛。
2915
+ SERIAL_SIGNING_EVAL_PROMPT = """你是独立小说质量评审(不要修改任何文件),不是番茄或七猫的签约编辑,不能代表平台结论。
2916
+ 评审稿件__NOTE__,按下方统一文本质量 rubric 评估;如附当前平台规则或编辑原话,可单独核对适配,不得臆测平台内部标准。
2765
2917
  请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
2766
2918
  {
2767
2919
  "scores": {"__DIMKEYS__"},
2768
2920
  "issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
2769
- "summary": "一句话总评:是否达到可申请签约评估的水平"
2921
+ "summary": "一句话内部文本诊断:优势、主要风险和未验证项;不得表述为平台结论或过签预测"
2770
2922
  }
2771
- 每个维度打 1-10 分(可为小数),按平台真实尺度从严。五个维度固定为:开篇吸引力、情节推进、文风统一、情感细腻度、节奏控制。
2772
- 驳回模板的高频话术——文风在风格上不统一(语域/句式跳变)、
2773
- 复杂情感描绘不够细腻(情绪概括无动作细节)、开篇切入点缺乏足够的吸引力(黄金一章未兑现)、情节的推动生硬(因果桥缺失/旁白硬推)、
2774
- 行文拖沓节奏缓慢(重复铺垫、与主线无关的描写)——每一条都要给出明确判定与证据。
2923
+ 统一评分 rubric:1-3 为明显影响理解或阅读的严重问题;4-6 为有具体可定位的弱项;7-8 为基本有效、仍有局部提升空间;9-10 为在本稿目标下表现突出。分数必须由所读文本支撑,不把分数映射成过签概率。
2924
+ 五个维度固定为:开篇吸引力、情节推进、文风统一、情感细腻度、节奏控制。每项给证据与未读范围;不能因不用某种套路(如黄金三章、打脸、单一视角)扣分。
2925
+ 可检查文风是否突变、情绪是否被概括、开篇承诺是否兑现、因果桥是否成立、重复是否损害节奏;这些是检查问题而不是预设缺陷。没有当前平台规则或明确编辑反馈时,不把质量评审包装成平台审核结论。
2775
2926
 
2776
2927
  ## 待评审稿件
2777
2928
  ---
@@ -4309,7 +4460,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
4309
4460
  agent.get("label"))
4310
4461
  time.sleep(0.15)
4311
4462
  gj = {"scores": {d: 8.0 for d in dims},
4312
- "issues": [], "summary": "(mock)全书结构完整,达到可签约水平"}
4463
+ "issues": [], "summary": "(mock)内部结构评估:整体结构完整;此结果不代表平台签约结论"}
4313
4464
  store.finish_step(run_id, step["n"], "done", summary="均分 8.0:(mock)全书达标",
4314
4465
  duration_s=0.15)
4315
4466
  results[idx] = gj
@@ -4326,7 +4477,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
4326
4477
  "- 本次送审批次为全书第 %d—%d 章(连载中段批次),文本中的前文节选仅供衔接核对;\n"
4327
4478
  "- 「黄金一章/黄金三章」口径仅适用于全书第 1—3 章,不得据此扣减本批批首章的开篇分;\n"
4328
4479
  "- 本批各章的既定事件以工作目录「章纲/」下的锁定章纲为准(可自行查阅):承段、"
4329
- "过渡章的节奏分应对照章纲既定事件是否落实与章末钩子是否成立,"
4480
+ "过渡章的节奏分应对照章纲既定事件与叙事功能是否落实;自然收束、留白均可,不以悬念钩子作为硬门槛。"
4330
4481
  "不以高潮段的事件密度为统一标准;\n"
4331
4482
  "- 「伏笔悬置」类 major 须先核对章纲的揭示位安排:揭示位写在后批章纲中的伏笔"
4332
4483
  "不算悬置,无任何安排的悬置才成立;\n"
@@ -4894,6 +5045,14 @@ def _run_content_review(run, task, agents, ev, stats, mode):
4894
5045
  pass
4895
5046
  else:
4896
5047
  is_research = task.get("type") == "research"
5048
+ # 经验库/知识库注入(review 引擎 13 类型补全,serial 起草同款):
5049
+ # learn_async 面向全部 run 收割,此前注入只在连载链——「全类型收割、
5050
+ # 单链注入」闭环半开。在 _draft_prompt_for 外取一次(bestof 多稿件
5051
+ # 共用一份,命中计数不重复累加);stable_order 保前缀缓存,预算纪律
5052
+ # 由块内自带(skills 包区限额+教训保底 / knowledge 装箱截断)。
5053
+ sk_lessons, _ = skills.block_for(task, stable_order=True, run_id=run_id)
5054
+ kb_block = knowledge.block_for(task)
5055
+ ctx_block = "\n\n".join(p for p in (sk_lessons, kb_block) if p)
4897
5056
 
4898
5057
  def _draft_prompt_for(vfile):
4899
5058
  p = (_tpl(task, "draft_prompt", NOVEL_DRAFT_PROMPT).replace("__FILE__", vfile)
@@ -4927,6 +5086,9 @@ def _run_content_review(run, task, agents, ev, stats, mode):
4927
5086
  gl = _gitlog_brief(workdir)
4928
5087
  if gl:
4929
5088
  p += ("\n\n## 本周提交素材(git log,如实取材,缺失勿虚构)\n" + gl)
5089
+ if ctx_block:
5090
+ # 空块不追加:零命中零噪音(空库时 prompt 与旧版字节一致)
5091
+ p += "\n\n" + ctx_block
4930
5092
  p += _content_contract(task)
4931
5093
  return p
4932
5094
 
@@ -4987,6 +5149,16 @@ def _run_content_review(run, task, agents, ev, stats, mode):
4987
5149
  .replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
4988
5150
  if task.get("context"):
4989
5151
  crit_prompt += "\n\n## 原始任务背景与附件参考\n" + task["context"]
5152
+ # 经验库/知识库注入(serial 逐章评审同款;「## 待评审稿件」锚点由
5153
+ # _ensure_critique_placeholders 保证存在),按证据强度随评审下发
5154
+ sk, _ = skills.block_for(task, stable_order=True, run_id=run_id)
5155
+ if sk:
5156
+ crit_prompt = crit_prompt.replace(
5157
+ "## 待评审稿件", "%s\n\n## 待评审稿件" % sk, 1)
5158
+ kb = knowledge.block_for(task)
5159
+ if kb:
5160
+ crit_prompt = crit_prompt.replace(
5161
+ "## 待评审稿件", "%s\n\n## 待评审稿件" % kb, 1)
4990
5162
  # AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
4991
5163
  # 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
4992
5164
  crit_prompt = aiflavor.inject_into_prompt(crit_prompt, manuscript, task.get("type"))
@@ -5182,41 +5354,26 @@ def _read_constitution(workdir):
5182
5354
  "与其他要求冲突时以宪章为准)\n\n" + txt + "\n\n")
5183
5355
 
5184
5356
 
5185
- def _write_project_memory(task, workdir, lines):
5186
- """项目记忆持久化(借鉴 agentmemory):代码任务成功后把架构事实追加到
5187
- .codebee/project-memory.md——同目录后续 code 任务规划前自动注入,
5188
- 让编排者「知道这个代码库的脾气」而非每次从零摸索。失败静默。"""
5357
+ def _write_project_memory(task, workdir, lines, source_run=""):
5358
+ """Persist proposed project facts; they are not injected before approval."""
5189
5359
  if not lines:
5190
5360
  return ""
5191
5361
  try:
5192
- pm = os.path.join(workdir, ".codebee", "project-memory.md")
5193
- os.makedirs(os.path.dirname(pm), exist_ok=True)
5194
- header_needed = not os.path.isfile(pm)
5195
- with open(pm, "a", encoding="utf-8") as f:
5196
- if header_needed:
5197
- f.write("# 项目记忆(每次代码任务完成后自动追加,供后续任务参考)\n\n")
5198
- f.write("### %s · %s\n" % (task.get("title") or "", _now()))
5199
- for ln in lines:
5200
- f.write("- %s\n" % str(ln)[:300])
5201
- f.write("\n")
5202
- return pm
5362
+ from . import project_memory
5363
+ row = project_memory.propose(workdir, source_run=source_run,
5364
+ title=task.get("title") or "", facts=lines)
5365
+ return str(project_memory._file(workdir)) if row else ""
5203
5366
  except Exception:
5204
5367
  return ""
5205
5368
 
5206
5369
 
5207
5370
  def _read_project_memory(workdir, cap=4000):
5208
- """读取项目记忆供规划提示词注入。超出上限截断到最新条目。"""
5209
- p = os.path.join(workdir or "", ".codebee", "project-memory.md")
5210
- if not _inside(workdir, p) or not os.path.isfile(p):
5211
- return ""
5371
+ """Only inject approved, unexpired and fingerprint-verified memory."""
5212
5372
  try:
5213
- txt = _read_text_any_enc(p)[:cap].strip()
5214
- except OSError:
5215
- return ""
5216
- if not txt:
5373
+ from . import project_memory
5374
+ return project_memory.active_text(workdir, cap=cap)
5375
+ except (OSError, ValueError, TypeError):
5217
5376
  return ""
5218
- return ("## 项目记忆(此前代码任务在此工作目录留下的架构事实,"
5219
- "规划时优先参考)\n\n" + txt + "\n\n")
5220
5377
 
5221
5378
 
5222
5379
  def _write_task_spec(task, workdir):
@@ -5347,10 +5504,24 @@ def _write_task_plan(task, workdir, plan):
5347
5504
  if root not in target.parents:
5348
5505
  return ""
5349
5506
  target.parent.mkdir(parents=True, exist_ok=True)
5507
+ # 断点续跑(B):旧计划已勾选 [x] 的项按标题继承到新计划——重试重新
5508
+ # 规划不再抹掉已完成进度(继承发生在写盘前,mark_task_plan 的行号
5509
+ # 约定基于新文件,安全);[!]/[>] 不继承,重跑自然重置。
5510
+ prev_done = {}
5511
+ try:
5512
+ if target.is_file():
5513
+ _pat = re.compile(r"^\d+\. \[([ x!>])\] (.+)$")
5514
+ for _ln in target.read_text("utf-8").splitlines():
5515
+ _m = _pat.match(_ln)
5516
+ if _m and _m.group(1) == "x":
5517
+ prev_done.setdefault(_m.group(2).strip(), True)
5518
+ except Exception:
5519
+ prev_done = {}
5350
5520
  lines = ["# 任务计划", "", "来源:%s" % (plan or {}).get("source", "?"), ""]
5351
5521
  # checkbox 形态:mark_task_plan 按行号翻 [x]/[!]/[>],断点一眼可见
5352
5522
  for i, s in enumerate((plan or {}).get("steps") or [], 1):
5353
- lines.append("%d. [ ] %s" % (i, str(s.get("detail") or s.get("title") or "")[:200]))
5523
+ _t = str(s.get("detail") or s.get("title") or "")[:200]
5524
+ lines.append("%d. [%s] %s" % (i, "x" if prev_done.get(_t.strip()) else " ", _t))
5354
5525
  target.write_text("\n".join(lines) + "\n", encoding="utf-8")
5355
5526
  return str(target)
5356
5527
  except Exception:
@@ -5414,6 +5585,79 @@ def mark_task_plan(workdir, index, status):
5414
5585
  return False
5415
5586
 
5416
5587
 
5588
+ def _same_workdir(a, b):
5589
+ """路径同位判断(Windows 大小写/斜杠不敏感);任一为空即不同。"""
5590
+ try:
5591
+ na = os.path.normcase(os.path.abspath(a or ""))
5592
+ nb = os.path.normcase(os.path.abspath(b or ""))
5593
+ return bool(na) and bool(nb) and na == nb
5594
+ except Exception:
5595
+ return False
5596
+
5597
+
5598
+ def _assemble_handoff(task, run):
5599
+ """断点简报组装(B):终态 run 的「走到哪/改了什么/还剩什么」快照。
5600
+ 存进 run 记录供 retry_task 继承;组装失败返回近空 dict,绝不挡收尾。"""
5601
+ steps = run.get("steps") or []
5602
+ done_steps = [{"role": s.get("role") or "", "summary": (s.get("summary") or "")[:120]}
5603
+ for s in steps if (s.get("status") or "") == "done"][-8:]
5604
+ pending = ""
5605
+ for s in steps:
5606
+ if (s.get("status") or "") != "done":
5607
+ pending = "%s%s" % (s.get("role") or "?",
5608
+ ("(%s)" % s["summary"][:100]) if s.get("summary") else "")
5609
+ break
5610
+ try:
5611
+ _total, _left = plan_stopgate(task.get("workdir") or "")
5612
+ except Exception:
5613
+ _total, _left = 0, []
5614
+ ch = run.get("changes")
5615
+ if not ch:
5616
+ try:
5617
+ from . import gitmod
5618
+ ch = gitmod.collect_changes(task.get("workdir") or "")
5619
+ except Exception:
5620
+ ch = {}
5621
+ files = [f.get("path") for f in ((ch or {}).get("files") or []) if f.get("path")][:25]
5622
+ return {
5623
+ "status": run.get("status") or "",
5624
+ "error": (run.get("error") or "")[:240],
5625
+ "ended_at": run.get("ended_at") or "",
5626
+ "done_steps": done_steps,
5627
+ "pending": pending,
5628
+ "plan_total": _total,
5629
+ "plan_left": [[n, m, t] for n, m, t in (_left or [])][:6],
5630
+ "changed_files": files,
5631
+ }
5632
+
5633
+
5634
+ def _handoff_brief(h):
5635
+ """断点简报 → 注入 __CONTEXT__ 的人话块;无实质内容返回空串(零噪音)。"""
5636
+ if not isinstance(h, dict) or not (h.get("done_steps") or h.get("pending")
5637
+ or h.get("plan_left") or h.get("changed_files")):
5638
+ return ""
5639
+ rows = ["## 断点简报(上次运行中断现场,系统注入)",
5640
+ "上次运行以「%s」终止%s。工作目录/任务分支保留着上次已完成的部分改动。"
5641
+ "先核对现场再继续:已完成的修复不要推翻重做,从中断处接着完成目标。"
5642
+ % (h.get("status") or "?",
5643
+ ("(原因:%s)" % h["error"]) if h.get("error") else "")]
5644
+ done = h.get("done_steps") or []
5645
+ if done:
5646
+ rows.append("- 已完成步骤:" + ";".join(
5647
+ ("%s:%s" % (s.get("role") or "?", s["summary"])) if s.get("summary")
5648
+ else (s.get("role") or "?") for s in done))
5649
+ if h.get("pending"):
5650
+ rows.append("- 中断位置:%s" % h["pending"])
5651
+ left = h.get("plan_left") or []
5652
+ if left:
5653
+ rows.append("- 活计划未完成项:" + "、".join(
5654
+ "#%s[%s]%s" % (n, m, t) for n, m, t in left))
5655
+ files = h.get("changed_files") or []
5656
+ if files:
5657
+ rows.append("- 上次已改动的文件:" + "、".join(str(f) for f in files))
5658
+ return "\n".join(rows)
5659
+
5660
+
5417
5661
  def _workdir_blocker(task, run_id):
5418
5662
  """同工作目录互斥:返回占用该目录的任务(无则 None)。
5419
5663
  任务计划/项目记忆/任务分支检出都是工作目录级共享状态,并行会互相覆盖
@@ -5505,6 +5749,11 @@ def execute_run(run_id):
5505
5749
  difficulty=task_spec["difficulty"])
5506
5750
  task = dict(task)
5507
5751
  task = attachments.refresh_task(task, task.get("workdir") or "")
5752
+ try:
5753
+ task, _agent_context = agent_context.inject(task, task.get("workdir") or "")
5754
+ store.update_run(run_id, agent_context=_agent_context)
5755
+ except Exception:
5756
+ pass
5508
5757
  task["_compiled_spec"] = task_spec
5509
5758
  # 运行内统一使用编译后的难度;store 中历史任务常带 difficulty=auto,
5510
5759
  # 不能让这个兼容值覆盖 easy/default/hard 的模型调度决策。
@@ -5708,6 +5957,17 @@ def execute_run(run_id):
5708
5957
  "restore_error": repr(e)[:200]})
5709
5958
  except Exception:
5710
5959
  pass
5960
+ # 断点简报(B):失败/取消/超时的代码 run 组装「走到哪/改了什么/还剩什么」,
5961
+ # 供 retry_task 继承注入新 run——重试从断点继续,不从零烧 token。
5962
+ # done 不组装:主动重跑语义就是重来。放在 git 收尾后,changes 是最新的。
5963
+ try:
5964
+ if (task.get("engine")
5965
+ or ("code" if task.get("type") == "code" else "review")) == "code":
5966
+ _fin = store.get_run(run_id) or {}
5967
+ if _fin.get("status") in ("failed", "cancelled", "timeout"):
5968
+ store.update_run(run_id, handoff=_assemble_handoff(task, _fin))
5969
+ except Exception:
5970
+ pass
5711
5971
  # 自学习闭环:运行结束自动把本次评审暴露的问题沉淀为可复用教训(异步,不阻塞)
5712
5972
  try:
5713
5973
  if store.get_run(run_id):