codebee 0.1.96 → 0.1.98
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 +13 -0
- package/README.md +4 -2
- package/app/core/agent_context.py +21 -3
- package/app/core/board.py +39 -2
- package/app/core/builtin_agent.py +409 -34
- package/app/core/checkpoints.py +505 -0
- package/app/core/jobs.py +8 -1
- package/app/core/manager.py +109 -11
- package/app/core/mcp_client.py +200 -29
- package/app/core/pipeline.py +289 -40
- package/app/core/policy.py +36 -4
- package/app/core/project_memory.py +139 -0
- package/app/core/publish/auto.py +80 -19
- package/app/core/publish/browser.py +39 -0
- package/app/core/publish/fanqie.py +2 -0
- package/app/core/publish/flow.py +96 -17
- package/app/core/publish/flows-fanqie-calibrated.json +23 -11
- package/app/core/publish/ledger.py +38 -1
- package/app/core/runner.py +21 -11
- package/app/core/settings.py +13 -5
- package/app/core/settings_schema.py +14 -3
- package/app/core/store.py +21 -6
- package/app/core/tool_outputs.py +107 -0
- package/app/core/wxdigest.py +8 -1
- package/app/main.py +76 -2
- package/app/ui/app.js +356 -82
- package/app/ui/assets/workbench-reference.png +0 -0
- package/app/ui/board.css +451 -666
- package/app/ui/board.html +97 -94
- package/app/ui/board.js +316 -112
- package/app/ui/hive3d.js +242 -0
- package/app/ui/i18n.js +93 -10
- package/app/ui/index.html +31 -3
- package/app/ui/style.css +97 -111
- package/package.json +3 -2
package/app/core/pipeline.py
CHANGED
|
@@ -229,10 +229,12 @@ def _resume_workdir(resume_ctx, fallback):
|
|
|
229
229
|
|
|
230
230
|
|
|
231
231
|
def _compaction_enabled():
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
if
|
|
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()
|
|
@@ -451,6 +453,30 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
451
453
|
except Exception:
|
|
452
454
|
pass
|
|
453
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
|
|
454
480
|
if dead_binding:
|
|
455
481
|
from .error_codes import ErrorCode
|
|
456
482
|
res = {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
|
|
@@ -492,11 +518,47 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
492
518
|
if directive_block:
|
|
493
519
|
prompt = directive_block + "\n\n---\n\n" + prompt
|
|
494
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
|
|
495
534
|
res = _spawn_step(session_run_id=run_id, role=role, agent=agent,
|
|
496
535
|
prompt=effective_prompt, workdir=workdir, readonly=readonly,
|
|
497
536
|
ev=ev, timeout=timeout, resume=resume, step=step,
|
|
498
537
|
log_abs=log_abs, images=images, require_tools=require_tools,
|
|
499
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
|
|
500
562
|
# 先收尾再查取消:取消时进程已被 run_process 杀停,若先抛 Cancelled,
|
|
501
563
|
# 步骤记录会永远停在「运行中」变僵尸(与 _run_verify 的顺序对齐)
|
|
502
564
|
_finish_step_result(run_id, step, res, role, agent, start)
|
|
@@ -508,6 +570,45 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
508
570
|
return res
|
|
509
571
|
|
|
510
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 run with their natural
|
|
578
|
+
permission set (workdir read/write, network allowed): they are rejected
|
|
579
|
+
only when the task policy is *narrower* than that — fail closed for
|
|
580
|
+
narrowed policies, not for defaults(实案:create_task 改为强制落默认
|
|
581
|
+
策略后,本闸把 89—96 批的连载起草整批拒成「没有经验证的 OS 文件隔
|
|
582
|
+
离」;默认策略与外部 CLI 的天然权限一致,不属于收窄).
|
|
583
|
+
"""
|
|
584
|
+
if (agent or {}).get("mode") == "mock":
|
|
585
|
+
return ""
|
|
586
|
+
from . import policy
|
|
587
|
+
root = os.path.realpath(workdir or ".")
|
|
588
|
+
normalized = policy.normalize_sandbox(sandbox, root)
|
|
589
|
+
if (agent or {}).get("kind") == "codex":
|
|
590
|
+
if normalized.get("network") is False:
|
|
591
|
+
return "沙箱拒绝:该 CLI 后端未接入网络隔离,拒绝运行网络受限任务"
|
|
592
|
+
if [os.path.realpath(x) for x in normalized.get("allowed_roots") or []] != [root]:
|
|
593
|
+
return "沙箱拒绝:该 CLI 后端未接入允许目录隔离,拒绝运行目录受限任务"
|
|
594
|
+
if normalized.get("disabled_tools"):
|
|
595
|
+
return "权限拒绝:该 CLI 后端无法执行 CodeBee 工具禁用策略,拒绝运行"
|
|
596
|
+
if normalized.get("env_allowlist"):
|
|
597
|
+
return "沙箱拒绝:该 CLI 后端未接入环境变量白名单,拒绝运行受限任务"
|
|
598
|
+
return ""
|
|
599
|
+
# 非 codex 外部 CLI:默认策略放行,收窄策略拒绝
|
|
600
|
+
if normalized.get("network") is False:
|
|
601
|
+
return "沙箱拒绝:该任务要求禁网,外部 CLI 无法执行网络受限任务"
|
|
602
|
+
if normalized.get("disabled_tools"):
|
|
603
|
+
return "权限拒绝:该任务带工具禁用策略,外部 CLI 无法执行"
|
|
604
|
+
if normalized.get("env_allowlist"):
|
|
605
|
+
return "沙箱拒绝:该任务带环境变量白名单,外部 CLI 无法执行受限任务"
|
|
606
|
+
roots = [os.path.realpath(x) for x in normalized.get("allowed_roots") or []]
|
|
607
|
+
if roots and root not in roots:
|
|
608
|
+
return "沙箱拒绝:该任务目录白名单不含工作目录,外部 CLI 无法保证隔离范围"
|
|
609
|
+
return ""
|
|
610
|
+
|
|
611
|
+
|
|
511
612
|
def _create_task_from_builtin(payload):
|
|
512
613
|
"""Create and enqueue a task requested by the direct conversation tool."""
|
|
513
614
|
task = store.create_task(payload)
|
|
@@ -587,7 +688,8 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
|
|
|
587
688
|
deadline=deadline, cancel_event=ev, log=_log, images=images,
|
|
588
689
|
on_reason=_on_reason, on_stream=_on_stream,
|
|
589
690
|
on_activity=_on_activity,
|
|
590
|
-
task_creator=_create_task_from_builtin
|
|
691
|
+
task_creator=_create_task_from_builtin,
|
|
692
|
+
sandbox=task.get("sandbox"), checkpoint_run_id=run_id)
|
|
591
693
|
if followups and res.get("ok"):
|
|
592
694
|
clean, fups = _parse_followups(res.get("text") or "")
|
|
593
695
|
if fups:
|
|
@@ -1461,6 +1563,28 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1461
1563
|
if project_memory:
|
|
1462
1564
|
task = dict(task, context=(task.get("context") or "") + "\n\n" + project_memory)
|
|
1463
1565
|
|
|
1566
|
+
# 断点简报(B):重试继承上一遍失败/中断 run 的现场,注入规划器与实现者
|
|
1567
|
+
# 共用的 __CONTEXT__——「走到哪、改了什么、还剩什么」不再从零重建上下文。
|
|
1568
|
+
_handoff = run.get("handoff") or {}
|
|
1569
|
+
_brief = _handoff_brief(_handoff)
|
|
1570
|
+
if _brief:
|
|
1571
|
+
task = dict(task, context=(task.get("context") or "") + "\n\n" + _brief)
|
|
1572
|
+
# 断点会话(A):上一遍实现者的 CLI 会话 id,implement_all 里按人复用。
|
|
1573
|
+
_inh_session = run.get("inherit_session") or {}
|
|
1574
|
+
|
|
1575
|
+
def _keep_sid(agent_row, agent_b, r):
|
|
1576
|
+
"""记录最后一次实现的会话 id:run 内 fix 轮复用 + 落盘 run 供重试跨 run 续会话。"""
|
|
1577
|
+
sid = _resume_sid(agent_b, r.get("sid"))
|
|
1578
|
+
if sid:
|
|
1579
|
+
impl_sid[0] = sid
|
|
1580
|
+
try:
|
|
1581
|
+
store.update_run(run_id, impl_session={
|
|
1582
|
+
"agent": agent_row.get("id"), "session": sid,
|
|
1583
|
+
"workdir": workdir})
|
|
1584
|
+
except Exception:
|
|
1585
|
+
pass
|
|
1586
|
+
return sid
|
|
1587
|
+
|
|
1464
1588
|
# ---- 规划。低风险短任务直接把目标作为单步计划,省掉一次独立模型调用;
|
|
1465
1589
|
# 附件或较长的用户背景仍先规划,避免快速路径漏读约束。
|
|
1466
1590
|
fast_path = (mode == "fast" or
|
|
@@ -1526,11 +1650,20 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1526
1650
|
|
|
1527
1651
|
def implement_all(impl_agent, prefix_note):
|
|
1528
1652
|
nonlocal reviewer
|
|
1529
|
-
#
|
|
1653
|
+
# 会话延续只对原实现者有效;换将后新智能体没有该会话,必须丢弃。
|
|
1654
|
+
# 优先级:任务级 resume(用户显式指定)> 断点继承会话(A:跨 run 续跑,
|
|
1655
|
+
# 仅同一实现者 + 同一工作目录 + 该 CLI 支持续会话时复用)
|
|
1656
|
+
inh_sid = ""
|
|
1657
|
+
if (not resume_ctx and _inh_session.get("session")
|
|
1658
|
+
and _inh_session.get("agent") == impl_agent["id"]
|
|
1659
|
+
and _same_workdir(_inh_session.get("workdir") or "", workdir)
|
|
1660
|
+
and _resume_sid(impl_agent, _inh_session.get("session"))):
|
|
1661
|
+
inh_sid = _inh_session["session"]
|
|
1530
1662
|
use_resume = (resume_ctx["session"]
|
|
1531
|
-
if (resume_ctx and impl_agent["id"] == resume_ctx["agent"]["id"])
|
|
1663
|
+
if (resume_ctx and impl_agent["id"] == resume_ctx["agent"]["id"])
|
|
1664
|
+
else (inh_sid or None))
|
|
1532
1665
|
# 续会话时 CLI 要在会话所属项目目录下启动,否则定位不到会话
|
|
1533
|
-
step_wd = _resume_workdir(resume_ctx, workdir) if use_resume else workdir
|
|
1666
|
+
step_wd = _resume_workdir(resume_ctx, workdir) if (resume_ctx and use_resume) else workdir
|
|
1534
1667
|
|
|
1535
1668
|
def _run_one(agt):
|
|
1536
1669
|
"""用指定智能体跑全部子任务;返回 (ok, 最后一次 res)。"""
|
|
@@ -1576,10 +1709,20 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1576
1709
|
note=prefix_note if i == 0 else "",
|
|
1577
1710
|
resume=use_resume, images=att_imgs,
|
|
1578
1711
|
require_tools=True, swap_guard=swap_guard)
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1712
|
+
_keep_sid(agt, agt_b, res)
|
|
1713
|
+
# 断点继承会话失效兜底(A):会话可能已被 CLI 清理或跨机不可续。
|
|
1714
|
+
# 认证/配额类死因换会话也没用,不重试;其余丢弃继承会话原样重跑一次。
|
|
1715
|
+
if (not res["ok"] and inh_sid and i == 0 and use_resume == inh_sid
|
|
1716
|
+
and not (res.get("raw") or {}).get("cancelled")
|
|
1717
|
+
and not runner._auth_error(res.get("error") or "")
|
|
1718
|
+
and not runner._quota_error(res.get("error") or "")):
|
|
1719
|
+
res = _run_step(run_id, role, agt_b, prompt, step_wd,
|
|
1720
|
+
readonly=False, ev=ev,
|
|
1721
|
+
note=((prefix_note + ";") if prefix_note else "")
|
|
1722
|
+
+ "继承会话续跑失败,已丢弃旧会话全新执行",
|
|
1723
|
+
resume=None, images=att_imgs,
|
|
1724
|
+
require_tools=True, swap_guard=swap_guard)
|
|
1725
|
+
_keep_sid(agt, agt_b, res)
|
|
1583
1726
|
if agt.get("mode") == "mock" and res["ok"]:
|
|
1584
1727
|
try:
|
|
1585
1728
|
mock_path = os.path.abspath(os.path.join(workdir, "mock-impl.txt"))
|
|
@@ -1606,10 +1749,11 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1606
1749
|
error=auth_err, ended_at=_now())
|
|
1607
1750
|
return False
|
|
1608
1751
|
# 单路实现失败:Best-of-N 赛马兜底(借鉴 orca worktree 择优)——多路并行
|
|
1609
|
-
# 各自 worktree 隔离实现+验证,胜者 diff
|
|
1752
|
+
# 各自 worktree 隔离实现+验证,胜者 diff 回主工作区;失败回落走换将。
|
|
1753
|
+
# 会话延续在场时跳过(N 路共用同一 CLI 会话会互相践踏,与连载同理)
|
|
1610
1754
|
if (mode == "auto" and impl_agent.get("mode") == "real"
|
|
1611
1755
|
and max(1, min(3, int(task.get("best_of") or 1))) >= 2
|
|
1612
|
-
and resume_ctx is None
|
|
1756
|
+
and resume_ctx is None and not inh_sid
|
|
1613
1757
|
and _code_bestof(run, task, impl_agent, difficulty, ev)):
|
|
1614
1758
|
return True
|
|
1615
1759
|
# 实现步失败不立刻判死:2026-09-16 实测配额烧干时 5 连跑全在同一条 CLI 上
|
|
@@ -1756,6 +1900,7 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1756
1900
|
note="自动修复第 %d 轮" % round_no,
|
|
1757
1901
|
resume=resume_ctx["session"] if resume_ctx else impl_sid[0],
|
|
1758
1902
|
require_tools=True)
|
|
1903
|
+
_keep_sid(impl, modelhub.bind_agent(impl, difficulty), res)
|
|
1759
1904
|
if (res.get("raw") or {}).get("timed_out"):
|
|
1760
1905
|
# 自动修复没有换将/重试兜底;超时后继续验收会把未完成的修复
|
|
1761
1906
|
# 当作正常轮次,让 run 继续显示 running,最终还可能误标 done。
|
|
@@ -1873,7 +2018,7 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1873
2018
|
_mem_lines = ["改动文件:%s" % (_touched_str or "(无 diff)"),
|
|
1874
2019
|
"验收:%s" % ("通过" if verify_pass else "未通过"),
|
|
1875
2020
|
"修复轮数:%d" % (len(repairs) - 1)]
|
|
1876
|
-
_write_project_memory(task, workdir, _mem_lines)
|
|
2021
|
+
_write_project_memory(task, workdir, _mem_lines, source_run=run_id)
|
|
1877
2022
|
except Exception:
|
|
1878
2023
|
pass
|
|
1879
2024
|
# Stop gate(planning-with-files):收工时活计划若有未勾选项,摘要与 verdict
|
|
@@ -4913,6 +5058,14 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
4913
5058
|
pass
|
|
4914
5059
|
else:
|
|
4915
5060
|
is_research = task.get("type") == "research"
|
|
5061
|
+
# 经验库/知识库注入(review 引擎 13 类型补全,serial 起草同款):
|
|
5062
|
+
# learn_async 面向全部 run 收割,此前注入只在连载链——「全类型收割、
|
|
5063
|
+
# 单链注入」闭环半开。在 _draft_prompt_for 外取一次(bestof 多稿件
|
|
5064
|
+
# 共用一份,命中计数不重复累加);stable_order 保前缀缓存,预算纪律
|
|
5065
|
+
# 由块内自带(skills 包区限额+教训保底 / knowledge 装箱截断)。
|
|
5066
|
+
sk_lessons, _ = skills.block_for(task, stable_order=True, run_id=run_id)
|
|
5067
|
+
kb_block = knowledge.block_for(task)
|
|
5068
|
+
ctx_block = "\n\n".join(p for p in (sk_lessons, kb_block) if p)
|
|
4916
5069
|
|
|
4917
5070
|
def _draft_prompt_for(vfile):
|
|
4918
5071
|
p = (_tpl(task, "draft_prompt", NOVEL_DRAFT_PROMPT).replace("__FILE__", vfile)
|
|
@@ -4946,6 +5099,9 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
4946
5099
|
gl = _gitlog_brief(workdir)
|
|
4947
5100
|
if gl:
|
|
4948
5101
|
p += ("\n\n## 本周提交素材(git log,如实取材,缺失勿虚构)\n" + gl)
|
|
5102
|
+
if ctx_block:
|
|
5103
|
+
# 空块不追加:零命中零噪音(空库时 prompt 与旧版字节一致)
|
|
5104
|
+
p += "\n\n" + ctx_block
|
|
4949
5105
|
p += _content_contract(task)
|
|
4950
5106
|
return p
|
|
4951
5107
|
|
|
@@ -5006,6 +5162,16 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
5006
5162
|
.replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
|
|
5007
5163
|
if task.get("context"):
|
|
5008
5164
|
crit_prompt += "\n\n## 原始任务背景与附件参考\n" + task["context"]
|
|
5165
|
+
# 经验库/知识库注入(serial 逐章评审同款;「## 待评审稿件」锚点由
|
|
5166
|
+
# _ensure_critique_placeholders 保证存在),按证据强度随评审下发
|
|
5167
|
+
sk, _ = skills.block_for(task, stable_order=True, run_id=run_id)
|
|
5168
|
+
if sk:
|
|
5169
|
+
crit_prompt = crit_prompt.replace(
|
|
5170
|
+
"## 待评审稿件", "%s\n\n## 待评审稿件" % sk, 1)
|
|
5171
|
+
kb = knowledge.block_for(task)
|
|
5172
|
+
if kb:
|
|
5173
|
+
crit_prompt = crit_prompt.replace(
|
|
5174
|
+
"## 待评审稿件", "%s\n\n## 待评审稿件" % kb, 1)
|
|
5009
5175
|
# AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
|
|
5010
5176
|
# 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
|
|
5011
5177
|
crit_prompt = aiflavor.inject_into_prompt(crit_prompt, manuscript, task.get("type"))
|
|
@@ -5201,41 +5367,26 @@ def _read_constitution(workdir):
|
|
|
5201
5367
|
"与其他要求冲突时以宪章为准)\n\n" + txt + "\n\n")
|
|
5202
5368
|
|
|
5203
5369
|
|
|
5204
|
-
def _write_project_memory(task, workdir, lines):
|
|
5205
|
-
"""
|
|
5206
|
-
.codebee/project-memory.md——同目录后续 code 任务规划前自动注入,
|
|
5207
|
-
让编排者「知道这个代码库的脾气」而非每次从零摸索。失败静默。"""
|
|
5370
|
+
def _write_project_memory(task, workdir, lines, source_run=""):
|
|
5371
|
+
"""Persist proposed project facts; they are not injected before approval."""
|
|
5208
5372
|
if not lines:
|
|
5209
5373
|
return ""
|
|
5210
5374
|
try:
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
if header_needed:
|
|
5216
|
-
f.write("# 项目记忆(每次代码任务完成后自动追加,供后续任务参考)\n\n")
|
|
5217
|
-
f.write("### %s · %s\n" % (task.get("title") or "", _now()))
|
|
5218
|
-
for ln in lines:
|
|
5219
|
-
f.write("- %s\n" % str(ln)[:300])
|
|
5220
|
-
f.write("\n")
|
|
5221
|
-
return pm
|
|
5375
|
+
from . import project_memory
|
|
5376
|
+
row = project_memory.propose(workdir, source_run=source_run,
|
|
5377
|
+
title=task.get("title") or "", facts=lines)
|
|
5378
|
+
return str(project_memory._file(workdir)) if row else ""
|
|
5222
5379
|
except Exception:
|
|
5223
5380
|
return ""
|
|
5224
5381
|
|
|
5225
5382
|
|
|
5226
5383
|
def _read_project_memory(workdir, cap=4000):
|
|
5227
|
-
"""
|
|
5228
|
-
p = os.path.join(workdir or "", ".codebee", "project-memory.md")
|
|
5229
|
-
if not _inside(workdir, p) or not os.path.isfile(p):
|
|
5230
|
-
return ""
|
|
5384
|
+
"""Only inject approved, unexpired and fingerprint-verified memory."""
|
|
5231
5385
|
try:
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
if not txt:
|
|
5386
|
+
from . import project_memory
|
|
5387
|
+
return project_memory.active_text(workdir, cap=cap)
|
|
5388
|
+
except (OSError, ValueError, TypeError):
|
|
5236
5389
|
return ""
|
|
5237
|
-
return ("## 项目记忆(此前代码任务在此工作目录留下的架构事实,"
|
|
5238
|
-
"规划时优先参考)\n\n" + txt + "\n\n")
|
|
5239
5390
|
|
|
5240
5391
|
|
|
5241
5392
|
def _write_task_spec(task, workdir):
|
|
@@ -5366,10 +5517,24 @@ def _write_task_plan(task, workdir, plan):
|
|
|
5366
5517
|
if root not in target.parents:
|
|
5367
5518
|
return ""
|
|
5368
5519
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
5520
|
+
# 断点续跑(B):旧计划已勾选 [x] 的项按标题继承到新计划——重试重新
|
|
5521
|
+
# 规划不再抹掉已完成进度(继承发生在写盘前,mark_task_plan 的行号
|
|
5522
|
+
# 约定基于新文件,安全);[!]/[>] 不继承,重跑自然重置。
|
|
5523
|
+
prev_done = {}
|
|
5524
|
+
try:
|
|
5525
|
+
if target.is_file():
|
|
5526
|
+
_pat = re.compile(r"^\d+\. \[([ x!>])\] (.+)$")
|
|
5527
|
+
for _ln in target.read_text("utf-8").splitlines():
|
|
5528
|
+
_m = _pat.match(_ln)
|
|
5529
|
+
if _m and _m.group(1) == "x":
|
|
5530
|
+
prev_done.setdefault(_m.group(2).strip(), True)
|
|
5531
|
+
except Exception:
|
|
5532
|
+
prev_done = {}
|
|
5369
5533
|
lines = ["# 任务计划", "", "来源:%s" % (plan or {}).get("source", "?"), ""]
|
|
5370
5534
|
# checkbox 形态:mark_task_plan 按行号翻 [x]/[!]/[>],断点一眼可见
|
|
5371
5535
|
for i, s in enumerate((plan or {}).get("steps") or [], 1):
|
|
5372
|
-
|
|
5536
|
+
_t = str(s.get("detail") or s.get("title") or "")[:200]
|
|
5537
|
+
lines.append("%d. [%s] %s" % (i, "x" if prev_done.get(_t.strip()) else " ", _t))
|
|
5373
5538
|
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
5374
5539
|
return str(target)
|
|
5375
5540
|
except Exception:
|
|
@@ -5433,6 +5598,79 @@ def mark_task_plan(workdir, index, status):
|
|
|
5433
5598
|
return False
|
|
5434
5599
|
|
|
5435
5600
|
|
|
5601
|
+
def _same_workdir(a, b):
|
|
5602
|
+
"""路径同位判断(Windows 大小写/斜杠不敏感);任一为空即不同。"""
|
|
5603
|
+
try:
|
|
5604
|
+
na = os.path.normcase(os.path.abspath(a or ""))
|
|
5605
|
+
nb = os.path.normcase(os.path.abspath(b or ""))
|
|
5606
|
+
return bool(na) and bool(nb) and na == nb
|
|
5607
|
+
except Exception:
|
|
5608
|
+
return False
|
|
5609
|
+
|
|
5610
|
+
|
|
5611
|
+
def _assemble_handoff(task, run):
|
|
5612
|
+
"""断点简报组装(B):终态 run 的「走到哪/改了什么/还剩什么」快照。
|
|
5613
|
+
存进 run 记录供 retry_task 继承;组装失败返回近空 dict,绝不挡收尾。"""
|
|
5614
|
+
steps = run.get("steps") or []
|
|
5615
|
+
done_steps = [{"role": s.get("role") or "", "summary": (s.get("summary") or "")[:120]}
|
|
5616
|
+
for s in steps if (s.get("status") or "") == "done"][-8:]
|
|
5617
|
+
pending = ""
|
|
5618
|
+
for s in steps:
|
|
5619
|
+
if (s.get("status") or "") != "done":
|
|
5620
|
+
pending = "%s%s" % (s.get("role") or "?",
|
|
5621
|
+
("(%s)" % s["summary"][:100]) if s.get("summary") else "")
|
|
5622
|
+
break
|
|
5623
|
+
try:
|
|
5624
|
+
_total, _left = plan_stopgate(task.get("workdir") or "")
|
|
5625
|
+
except Exception:
|
|
5626
|
+
_total, _left = 0, []
|
|
5627
|
+
ch = run.get("changes")
|
|
5628
|
+
if not ch:
|
|
5629
|
+
try:
|
|
5630
|
+
from . import gitmod
|
|
5631
|
+
ch = gitmod.collect_changes(task.get("workdir") or "")
|
|
5632
|
+
except Exception:
|
|
5633
|
+
ch = {}
|
|
5634
|
+
files = [f.get("path") for f in ((ch or {}).get("files") or []) if f.get("path")][:25]
|
|
5635
|
+
return {
|
|
5636
|
+
"status": run.get("status") or "",
|
|
5637
|
+
"error": (run.get("error") or "")[:240],
|
|
5638
|
+
"ended_at": run.get("ended_at") or "",
|
|
5639
|
+
"done_steps": done_steps,
|
|
5640
|
+
"pending": pending,
|
|
5641
|
+
"plan_total": _total,
|
|
5642
|
+
"plan_left": [[n, m, t] for n, m, t in (_left or [])][:6],
|
|
5643
|
+
"changed_files": files,
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
|
|
5647
|
+
def _handoff_brief(h):
|
|
5648
|
+
"""断点简报 → 注入 __CONTEXT__ 的人话块;无实质内容返回空串(零噪音)。"""
|
|
5649
|
+
if not isinstance(h, dict) or not (h.get("done_steps") or h.get("pending")
|
|
5650
|
+
or h.get("plan_left") or h.get("changed_files")):
|
|
5651
|
+
return ""
|
|
5652
|
+
rows = ["## 断点简报(上次运行中断现场,系统注入)",
|
|
5653
|
+
"上次运行以「%s」终止%s。工作目录/任务分支保留着上次已完成的部分改动。"
|
|
5654
|
+
"先核对现场再继续:已完成的修复不要推翻重做,从中断处接着完成目标。"
|
|
5655
|
+
% (h.get("status") or "?",
|
|
5656
|
+
("(原因:%s)" % h["error"]) if h.get("error") else "")]
|
|
5657
|
+
done = h.get("done_steps") or []
|
|
5658
|
+
if done:
|
|
5659
|
+
rows.append("- 已完成步骤:" + ";".join(
|
|
5660
|
+
("%s:%s" % (s.get("role") or "?", s["summary"])) if s.get("summary")
|
|
5661
|
+
else (s.get("role") or "?") for s in done))
|
|
5662
|
+
if h.get("pending"):
|
|
5663
|
+
rows.append("- 中断位置:%s" % h["pending"])
|
|
5664
|
+
left = h.get("plan_left") or []
|
|
5665
|
+
if left:
|
|
5666
|
+
rows.append("- 活计划未完成项:" + "、".join(
|
|
5667
|
+
"#%s[%s]%s" % (n, m, t) for n, m, t in left))
|
|
5668
|
+
files = h.get("changed_files") or []
|
|
5669
|
+
if files:
|
|
5670
|
+
rows.append("- 上次已改动的文件:" + "、".join(str(f) for f in files))
|
|
5671
|
+
return "\n".join(rows)
|
|
5672
|
+
|
|
5673
|
+
|
|
5436
5674
|
def _workdir_blocker(task, run_id):
|
|
5437
5675
|
"""同工作目录互斥:返回占用该目录的任务(无则 None)。
|
|
5438
5676
|
任务计划/项目记忆/任务分支检出都是工作目录级共享状态,并行会互相覆盖
|
|
@@ -5732,6 +5970,17 @@ def execute_run(run_id):
|
|
|
5732
5970
|
"restore_error": repr(e)[:200]})
|
|
5733
5971
|
except Exception:
|
|
5734
5972
|
pass
|
|
5973
|
+
# 断点简报(B):失败/取消/超时的代码 run 组装「走到哪/改了什么/还剩什么」,
|
|
5974
|
+
# 供 retry_task 继承注入新 run——重试从断点继续,不从零烧 token。
|
|
5975
|
+
# done 不组装:主动重跑语义就是重来。放在 git 收尾后,changes 是最新的。
|
|
5976
|
+
try:
|
|
5977
|
+
if (task.get("engine")
|
|
5978
|
+
or ("code" if task.get("type") == "code" else "review")) == "code":
|
|
5979
|
+
_fin = store.get_run(run_id) or {}
|
|
5980
|
+
if _fin.get("status") in ("failed", "cancelled", "timeout"):
|
|
5981
|
+
store.update_run(run_id, handoff=_assemble_handoff(task, _fin))
|
|
5982
|
+
except Exception:
|
|
5983
|
+
pass
|
|
5735
5984
|
# 自学习闭环:运行结束自动把本次评审暴露的问题沉淀为可复用教训(异步,不阻塞)
|
|
5736
5985
|
try:
|
|
5737
5986
|
if store.get_run(run_id):
|
package/app/core/policy.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
2
|
+
"""Execution policy primitives shared by tools and runners."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
@@ -10,14 +10,24 @@ def normalize_sandbox(spec=None, workdir=None):
|
|
|
10
10
|
spec = spec if isinstance(spec, dict) else {}
|
|
11
11
|
root = Path(workdir or os.getcwd()).expanduser().resolve()
|
|
12
12
|
roots = []
|
|
13
|
-
|
|
13
|
+
requested_roots = spec.get("allowed_roots")
|
|
14
|
+
if requested_roots is not None and not isinstance(requested_roots, (list, tuple)):
|
|
15
|
+
raise ValueError("allowed_roots 必须是路径列表")
|
|
16
|
+
explicit_roots = bool(requested_roots)
|
|
17
|
+
for value in requested_roots or [str(root)]:
|
|
14
18
|
try:
|
|
15
19
|
candidate = Path(value).expanduser().resolve()
|
|
16
20
|
except (TypeError, OSError, ValueError):
|
|
17
21
|
continue
|
|
22
|
+
# The task payload is not an authority to grant host filesystem access.
|
|
23
|
+
# Sandbox roots may narrow the selected workspace, never widen it.
|
|
24
|
+
if candidate != root and root not in candidate.parents:
|
|
25
|
+
continue
|
|
18
26
|
if candidate not in roots:
|
|
19
27
|
roots.append(candidate)
|
|
20
28
|
if not roots:
|
|
29
|
+
if explicit_roots:
|
|
30
|
+
raise ValueError("allowed_roots 没有工作目录内的有效路径")
|
|
21
31
|
roots = [root]
|
|
22
32
|
env = []
|
|
23
33
|
for value in spec.get("env_allowlist") or []:
|
|
@@ -32,9 +42,31 @@ def normalize_sandbox(spec=None, workdir=None):
|
|
|
32
42
|
output = max(1024, min(100 * 1024 * 1024, int(spec.get("max_output_bytes") or 10 * 1024 * 1024)))
|
|
33
43
|
except (TypeError, ValueError):
|
|
34
44
|
output = 10 * 1024 * 1024
|
|
45
|
+
network_value = spec.get("network", True)
|
|
46
|
+
if isinstance(network_value, bool):
|
|
47
|
+
network = network_value
|
|
48
|
+
elif isinstance(network_value, str) and network_value.strip().lower() in ("true", "false"):
|
|
49
|
+
network = network_value.strip().lower() == "true"
|
|
50
|
+
else:
|
|
51
|
+
# Invalid policy input must never silently widen network access.
|
|
52
|
+
network = False
|
|
35
53
|
return {"allowed_roots": [str(x) for x in roots], "env_allowlist": env,
|
|
36
|
-
"network":
|
|
37
|
-
"max_output_bytes": output
|
|
54
|
+
"network": network, "timeout_s": timeout,
|
|
55
|
+
"max_output_bytes": output,
|
|
56
|
+
"disabled_tools": normalize_disabled_tools(spec.get("disabled_tools"))}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def normalize_disabled_tools(items):
|
|
60
|
+
if not isinstance(items, (list, tuple, set)):
|
|
61
|
+
return []
|
|
62
|
+
out = []
|
|
63
|
+
for value in items:
|
|
64
|
+
name = str(value or "").strip()[:120]
|
|
65
|
+
if name and name not in out:
|
|
66
|
+
out.append(name)
|
|
67
|
+
if len(out) >= 200:
|
|
68
|
+
break
|
|
69
|
+
return out
|
|
38
70
|
|
|
39
71
|
|
|
40
72
|
def path_allowed(path, sandbox):
|